Skip to content
This repository has been archived by the owner on Oct 10, 2022. It is now read-only.

Commit

Permalink
Refactor user manual to make it less confusing
Browse files Browse the repository at this point in the history
  • Loading branch information
smolijar committed Jan 4, 2019
1 parent b49cee2 commit 8579567
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 98 deletions.
123 changes: 25 additions & 98 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,22 @@ Tool for developing generic service layer for RESTful CRUD API in your Node.js b

## Why Crudella

Assume we have an entity repository (or any other interface for REST resource, allowing us to implement CRUD operations), we cannot just bind it to express routes.
Here are several random problems we might face:
Crudella is a service factory. It _creates_ your service module based on the minimal required configuration you provide.
Assume you have an entity repository (or any other CRUD interface for resource) and to make it work, you cannot simply bind it to routes, skipping through controller and service layer.
Here are several random problems you face in the mentioned layers:
- User authorization
- Request validation (this resource cannot be deleted, only some attributes can be updated etc.)
- Data transformation (before creating or updating resource)
- Verify that resource exists before update or delete
- Pass custom HTTP context to the validation functions

Crudella goes as far as possible to help you with menial repeated code, but hopefully not as far as to completely take over your application and magically orchestrate behind your back.
It helps you to remove lot of boilerplate code, allowing you to test the parts of your application that matter instead of writing lengthy integration tests for boring CRUD and helps you decouple manipulation with the storage (database repository) from business logic (validation, authorization etc.), leading to more organized code.
Crudella implements logic equivalent of the controller and the service layer.
It goes as far as possible to help you with menial repeated code, but hopefully not as much as to completely take over your application and magically orchestrate behind your back.

Crudella helps you in various ways:
- Remove lot of boilerplate code: This not only saves you work, but also gives you space to focus on the non-generic code.
- Simplifies testing: Don't waste your time on running lengthy integration tests for boring CRUD over and over again. Test business logic, not boilerplate.
- Separation of concerns: Do not mix your service logic with validation or authorization. Crudella encourages you to decouple your service logic from client validation and orchestrates the calls for you.

## Usage

Expand All @@ -38,7 +44,6 @@ Crudella is tested on several major Node.js [versions](https://travis-ci.com/Ack

## Getting started

Crudella is a service factory. It _creates_ your service module based on the minimal required configuration you provide.

```typescript
// dalmatianService.ts
Expand Down Expand Up @@ -69,48 +74,11 @@ router.use(createPupperMiddleware('/api/puppies'))
That saved us some time. We don't have to bind the routes ourselves.
More importantly though, we can control the flow using rich contextual data. See [authorization](#authorization).

Not using express? You can still use Crudella to save you some time. See [generating handlers](#generating-handlers).

## Advanced topics

### Generating handlers
See the following example to learn how to use it to create service handlers, binding your data repository methods to Crudella.

```typescript
// dalmatianService.ts
import { createService } from 'crudella';

const dalmatianService = createService<DalmatianAttributes>({
// each method receives relevant CRUD context
detail: ctx => dalmatianRepository.find(ctx.id, ctx.options),
create: ctx => dalmatianRepository.create(ctx.data, ctx.options),
update: ctx => dalmatianRepository.updateById(ctx.entity.id, ctx.data, ctx.options),
delete: ctx => dalmatianRepository.deleteById(ctx.entity.id),
list: ctx => dalmatianRepository.list(ctx.filters, ctx.options),
});

// dalmatianService.xxxHandler is a handler creator, called with options
export const dalmatianDetail = dalmatianService.detailHandler({ withRelated: withRelated.detail })
export const dalmatianCreate = dalmatianService.createHandler({ withRelated: withRelated.detail })
export const dalmatianUpdate = dalmatianService.updateHandler({ withRelated: withRelated.detail })
export const dalmatianDelete = dalmatianService.deleteHandler()
export const dalmatianList = dalmatianService.listHandler({ withRelated: withRelated.list })
```

No we have created a service exporting service-layer handlers for CRUD API on dalmatians.
Handlers are async and always return a promise with the result you provide from your bindings, or an error.
Here are are signatures of the service handlers (promise values depend on your implementation).
```typescript
dalmatianDetail: (id: number, context: C) => Promise<DalmatianAttributes>;
dalmatianCreate: (data: any, context: C) => Promise<DalmatianAttributes>;
dalmatianUpdate: (id: number, data: any, context: C) => Promise<DalmatianAttributes>;
dalmatianDelete: (id: number, context: C) => Promise<bool>;
dalmatianList: (filters: any, context: C) => Promise<DalmatianAttributes[]>;
```
The `C` type is any HTTP context you prefer to use, it can contain client parameters, user session etc.
:information_source: Not using express? You can still use Crudella. See [generating handlers](./guide/handlers.md).

It does not do much yet apart from passing options for now (but it provides a uniform interface :point_up:), but let's check [authorization](./authorization).
:information_source: This definition still feels too long and you have consistent repository API? See [configuring crudella using repository](./guide/repository.md).

## Advanced topics

### Authorization

Expand Down Expand Up @@ -150,76 +118,35 @@ const dalmatianService = createService<DalmatianAttributes>({

For full reference of the contexts, see [API docs](https://ackeecz.github.io/interfaces/basecrudcontext.html).

### Other topics

### Data transformation

```typescript
const aggregateMuzzles = (pupper: DalmatianAttributes) => ({
...pupper,
muzzleScore: pupper.muzzleLenght * pupper.age,
muzzleCount: 1,
})

const dalmatianService = createService<DalmatianAttributes>({
create: ctx => dalmatianRepository.create(aggregateMuzzles(ctx.data), ctx.options),
update: ctx => dalmatianRepository.updateById(ctx.entity.id, aggregateMuzzles(ctx.data), ctx.options),
});
```

But thats boring, repetitive and you might forget to do that on update etc. Use `processData` instead.

```typescript
const aggregateMuzzles = /*...*/

const dalmatianService = createService<DalmatianAttributes>({
create: ctx => dalmatianRepository.create(ctx.data, ctx.options),
update: ctx => dalmatianRepository.updateById(ctx.entity.id, ctx.data, ctx.options),
processData: ctx => aggregateMuzzles(ctx.data),
});
```

### Repository

If you are using an existing ORM or any data layer with consistent API you can take a shortcut when defining a Crudella service, using an option `repository`.
Create a bridge function for you data abstraction layer.

```typescript
export const bridgeRepo = <T extends {id: any}>(repo: MyRepo<T>) => ({
create: repo.create.withDetailById,
deleteById: repo.deleteById,
detailById: repo.detailById,
list: repo.list,
updateById: repo.updateById.withDetail,
});
```
Having this bridge function in your project, you can easily create CRUD services more consciously and focus on the hard stuff.

```typescript
const dalmatianService = createService<DalmatianAttributes>({
repository: bridgeRepo(dalmatianRepository),
});
```

- [Data transformation](./guide/data-transformation.md)
- [Configuring crudella using repository](./guide/repository.md)
- [Generating handlers](./guide/handlers.md)
- :construction: Service builder and custom context
- :construction: Dynamic options
- :construction: Custom 404 error
- :construction: Customize controller flow

## Development

### Building
#### Building

Run `npm run build` to compile Typescript into JavaScript.

### Testing
#### Testing

Project uses [Jest](https://jestjs.io) testing framework and its snapshot testing.
Run `npm run test` to test or `npm run test:coverage` to collect coverage.

[Travis CI](https://travis-ci.com/AckeeCZ/crudella) tests PRs, [Coveralls](https://coveralls.io/github/AckeeCZ/crudella?branch=master) collect coverage.

### Coding style
#### Coding style

TS lint and prettier
`npm run lint`

### Docs
#### Docs

To generate API docs using [TypeDoc](https://typedoc.org/) and preview locally, run `npm run docs`.
Output is an ignored `docs` folder.
Expand Down
26 changes: 26 additions & 0 deletions guide/data-transformation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Data transformation

```typescript
const aggregateMuzzles = (pupper: DalmatianAttributes) => ({
...pupper,
muzzleScore: pupper.muzzleLenght * pupper.age,
muzzleCount: 1,
})

const dalmatianService = createService<DalmatianAttributes>({
create: ctx => dalmatianRepository.create(aggregateMuzzles(ctx.data), ctx.options),
update: ctx => dalmatianRepository.updateById(ctx.entity.id, aggregateMuzzles(ctx.data), ctx.options),
});
```

But thats boring, repetitive and you might forget to do that on update etc. Use `processData` instead.

```typescript
const aggregateMuzzles = /*...*/

const dalmatianService = createService<DalmatianAttributes>({
create: ctx => dalmatianRepository.create(ctx.data, ctx.options),
update: ctx => dalmatianRepository.updateById(ctx.entity.id, ctx.data, ctx.options),
processData: ctx => aggregateMuzzles(ctx.data),
});
```
37 changes: 37 additions & 0 deletions guide/handlers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Generating handlers

Perhaps you don't want to use Express middleware but still want to use Crudella.
Well luckily it provides handler creators you can use and bind them yourself or use any other framework except express.

```typescript
// dalmatianService.ts
import { createService } from 'crudella';

const dalmatianService = createService<DalmatianAttributes>({
// each method receives relevant CRUD context
detail: ctx => dalmatianRepository.find(ctx.id, ctx.options),
create: ctx => dalmatianRepository.create(ctx.data, ctx.options),
update: ctx => dalmatianRepository.updateById(ctx.entity.id, ctx.data, ctx.options),
delete: ctx => dalmatianRepository.deleteById(ctx.entity.id),
list: ctx => dalmatianRepository.list(ctx.filters, ctx.options),
});

// dalmatianService.xxxHandler is a handler creator, called with optional options
export const dalmatianDetail = dalmatianService.detailHandler({ withRelated: withRelated.detail })
export const dalmatianCreate = dalmatianService.createHandler({ withRelated: withRelated.detail })
export const dalmatianUpdate = dalmatianService.updateHandler({ withRelated: withRelated.detail })
export const dalmatianDelete = dalmatianService.deleteHandler()
export const dalmatianList = dalmatianService.listHandler({ withRelated: withRelated.list })
```

No we have created a service exporting service-layer handlers for CRUD API on dalmatians.
Handlers are async and always return a promise with the result you provide from your bindings, or an error.
Here are are signatures of the service handlers (promise values depend on your implementation).
```typescript
dalmatianDetail: (id: number, context: C) => Promise<DalmatianAttributes>;
dalmatianCreate: (data: any, context: C) => Promise<DalmatianAttributes>;
dalmatianUpdate: (id: number, data: any, context: C) => Promise<DalmatianAttributes>;
dalmatianDelete: (id: number, context: C) => Promise<bool>;
dalmatianList: (filters: any, context: C) => Promise<DalmatianAttributes[]>;
```
The `C` type is any HTTP context you prefer to use, it can contain client parameters, user session etc.
21 changes: 21 additions & 0 deletions guide/repository.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Repository

If you are using an existing ORM or any data layer with consistent API you can take a shortcut when defining a Crudella service, using an option `repository`.
Create a bridge function for you data abstraction layer.

```typescript
export const bridgeRepo = <T extends {id: any}>(repo: MyRepo<T>) => ({
create: repo.create.withDetailById,
deleteById: repo.deleteById,
detailById: repo.detailById,
list: repo.list,
updateById: repo.updateById.withDetail,
});
```
Having this bridge function in your project, you can easily create CRUD services more consciously and focus on the hard stuff.

```typescript
const dalmatianService = createService<DalmatianAttributes>({
repository: bridgeRepo(dalmatianRepository),
});
```

0 comments on commit 8579567

Please sign in to comment.