Skip to content

Commit

Permalink
feat(): adds pagination service (#1)
Browse files Browse the repository at this point in the history
* feat(): adds pagination service

This adds pagination service to the got service in an attempt to
immitate the original got package.

* docs(pagination): updates readme documentation

This updates the readme documentation with usage example for the
pagination feature.

Co-authored-by: B'Tunde Aromire <babatunde.aromire@onacrefund.org>
  • Loading branch information
toondaey and B'Tunde Aromire authored Sep 29, 2020
1 parent 72c4f21 commit 12a7df5
Show file tree
Hide file tree
Showing 15 changed files with 385 additions and 57 deletions.
47 changes: 40 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ This is a simple nestjs module that exposes the [got](https://www.npmjs.com/pack
<p align='center'>
<a href="https://www.npmjs.com/package/@t00nday/nestjs-got" target='_blank'><img alt="npm" src="https://img.shields.io/npm/dm/@t00nday/nestjs-got" alt="NPM Downloads"></a>
<a href="https://coveralls.io/github/toondaey/nestjs-got" target="_blank" rel="noopener noreferrer"><img alt="Coveralls github" src="https://img.shields.io/coveralls/github/toondaey/nestjs-got"></a>
<a href="https://www.npmjs.com/package/@t00nday/nestjs-got" target="_blank" rel="noopener noreferrer"><img alt="npm version" src="https://img.shields.io/npm/v/@t00nday/nestjs-got"></a>
<a href="https://www.npmjs.com/package/@t00nday/nestjs-got" target="_blank" rel="noopener noreferrer"><img alt="npm version" src="https://img.shields.io/npm/v/@t00nday/nestjs-got?color=%234CC61E&label=NPM&logo=NPM"></a>
<a href="https://www.npmjs.com/package/@t00nday/nestjs-got" target="_blank" rel="noopener noreferrer"><img alt="LICENCE" src="https://img.shields.io/npm/l/@t00nday/nestjs-got"></a>
<a href="https://circleci.com/gh/toondaey/nestjs-got" target="_blank" rel="noopener noreferrer"><img alt="CircleCI build" src="https://img.shields.io/circleci/build/gh/toondaey/nestjs-got/master"></a>
<a href="https://www.npmjs.com/package/@t00nday/nestjs-got" target="_blank" rel="noopener noreferrer"><img alt="npm bundle size (scoped)" src="https://img.shields.io/bundlephobia/min/@t00nday/nestjs-got?color=#4CC61E"></a>
</p>

<details>
Expand Down Expand Up @@ -142,14 +143,14 @@ The `GotModuleOptions` is an alias for the `got` package's `ExtendOptions` hence

## API Methods

The module currently only exposes the basic JSON HTTP verbs through the GotService i.e. `get`, `head`, `post`, `put`, `patch` and `delete`.
The module currently only exposes the basic JSON HTTP verbs, as well as the pagination methods through the `GotService`.

All these methods support the same argument inputs i.e.:
For all JSON HTTP verbs - `get`, `head`, `post`, `put`, `patch` and `delete` - which are also the exposed methods, below is the the method signature where `method: string` **MUST** be any of their corresponding verbs.

```ts
// This is just used to explain the methods as this code doesn't exist in the package
import { Observable } from 'rxjs';
immport { Response, OptionsOfJSONResponseBody } from 'got';
import { Response, OptionsOfJSONResponseBody } from 'got';

interface GotServiceInterface {
[method: string]: (
Expand All @@ -159,13 +160,45 @@ interface GotServiceInterface {
}
```

For all pagination methods - `each` and `all`, below is the method signature each of them.

```ts
// This is just used to explain the methods as this code doesn't exist in the package
import { Observable } from 'rxjs';
import { Response, OptionsOfJSONResponseBody } from 'got';

interface GotServiceInterface {
[method: string]: <T = any, R = unknown>(
url: string | URL,
options?: OptionsWithPagination<T, R>,
) => Observable<T | T[]>;
}
```

A usage example of would be:

```ts
@Controller()
export class ExampleController {
constructor(private readonly gotService: GotService) {}

controllerMethod() {
// ...
this.gotService.pagination.all(someUrl, withOptions); // Returns Observable<T[]>
// or
this.gotService.pagination.each(someUrl, withOptions); // Returns Observable<T>
// ...
}
}
```

For more information of the usage pattern, please check [here](https://www.npmjs.com/package/got#pagination-1)

## ToDos

As stated above, this module only support some http verbs, however, the following are still in progress:

1. Support for pagination

2. Support for a `StreamService` which is supported by the **got** package itself.
1. Support for a `StreamService` which is supported by the **got** package itself.

## Contributing

Expand Down
9 changes: 9 additions & 0 deletions lib/abstrace.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Got, InstanceDefaults } from 'got';

export abstract class AbstractService {
readonly defaults: InstanceDefaults;

constructor(protected readonly got: Got) {
this.defaults = this.got.defaults;
}
}
1 change: 1 addition & 0 deletions lib/addons/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './rxjs';
1 change: 1 addition & 0 deletions lib/addons/rxjs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './scheduleAsyncIterable';
27 changes: 27 additions & 0 deletions lib/addons/rxjs/scheduleAsyncIterable.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { asapScheduler } from 'rxjs';
import { take } from 'rxjs/operators';

import { scheduledAsyncIterable } from './scheduleAsyncIterable';

describe('scheduleAsyncIterable()', () => {
it('', () => {
const iterator = async function* () {
let i = 1;
while (true) {
yield i;
i += 1;
}
};

const iterable = iterator();
let count = 1;

scheduledAsyncIterable<number>(iterable, asapScheduler)
.pipe(take(5))
.subscribe({
next(v) {
expect(v).toEqual(count++);
},
});
});
});
28 changes: 28 additions & 0 deletions lib/addons/rxjs/scheduleAsyncIterable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Observable, Subscriber, Subscription, SchedulerLike } from 'rxjs';

export const scheduledAsyncIterable = <T = any>(
input: AsyncIterable<T> | AsyncGenerator<T>,
scheduler: SchedulerLike,
): Observable<T> => {
return new Observable<T>((subscriber: Subscriber<T>) => {
const subscription = new Subscription();
subscription.add(
scheduler.schedule(() => {
const iterator = input[Symbol.asyncIterator]();
subscription.add(
scheduler.schedule(function () {
iterator.next().then((result: IteratorResult<T>) => {
if (result.done) {
subscriber.complete();
} else {
subscriber.next(result.value);
this.schedule();
}
});
}),
);
}),
);
return subscription;
});
};
3 changes: 2 additions & 1 deletion lib/got.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import {
GotModuleAsyncOptions,
GotModuleOptionsFactory,
} from './got.interface';
import { PaginationService } from './paginate.service';
import { GOT_INSTANCE, GOT_OPTIONS } from './got.constant';

@Module({
providers: [GotService],
providers: [GotService, PaginationService],
exports: [GotService],
})
export class GotModule {
Expand Down
7 changes: 6 additions & 1 deletion lib/got.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as faker from 'faker';
import { Got, HTTPError, Response } from 'got/dist/source';
import { Got, HTTPError, Response } from 'got';
import { Test, TestingModule } from '@nestjs/testing';

import { GotService } from './got.service';
import { GOT_INSTANCE } from './got.constant';
import { PaginationService } from './paginate.service';

describe('GotService', () => {
let service: GotService;
Expand All @@ -22,6 +23,10 @@ describe('GotService', () => {
provide: GOT_INSTANCE,
useValue: gotInstance,
},
{
provide: PaginationService,
useValue: {},
},
],
}).compile();

Expand Down
63 changes: 34 additions & 29 deletions lib/got.service.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import {
Got,
Response,
InstanceDefaults,
CancelableRequest,
OptionsOfJSONResponseBody,
} from 'got';
import { Observable, Subscriber } from 'rxjs';
import { Inject, Injectable } from '@nestjs/common';

import { GOT_INSTANCE } from './got.constant';
import { AbstractService } from './abstrace.service';
import { PaginationService } from './paginate.service';

@Injectable()
export class GotService {
readonly defaults: InstanceDefaults;
private _request!: CancelableRequest;
export class GotService extends AbstractService {
protected _request!: CancelableRequest<Response<any>>;

constructor(@Inject(GOT_INSTANCE) private readonly got: Got) {
this.defaults = this.got.defaults;
constructor(
@Inject(GOT_INSTANCE) got: Got,
readonly pagination: PaginationService,
) {
super(got);
}

head<T = Record<string, any> | []>(
Expand Down Expand Up @@ -70,33 +73,35 @@ export class GotService {
url: string | URL,
options?: OptionsOfJSONResponseBody,
): Observable<Response<T>> {
this._request = this.got[method](url, {
this._request = this.got[method]<T>(url, {
...options,
responseType: 'json',
...this.defaults,
});

return new Observable((subscriber: Subscriber<any>) => {
this._request
.then(response => subscriber.next(response))
.catch(
(
error: Pick<
Got,
| 'ReadError'
| 'HTTPError'
| 'ParseError'
| 'CacheError'
| 'UploadError'
| 'CancelError'
| 'RequestError'
| 'TimeoutError'
| 'MaxRedirectsError'
| 'UnsupportedProtocolError'
>,
) => subscriber.error(error),
)
.finally(() => subscriber.complete());
});
return new Observable<Response<T>>(
(subscriber: Subscriber<Response<T>>) => {
this._request
.then((response: Response<T>) => subscriber.next(response))
.catch(
(
error: Pick<
Got,
| 'ReadError'
| 'HTTPError'
| 'ParseError'
| 'CacheError'
| 'UploadError'
| 'CancelError'
| 'RequestError'
| 'TimeoutError'
| 'MaxRedirectsError'
| 'UnsupportedProtocolError'
>,
) => subscriber.error(error),
)
.finally(() => subscriber.complete());
},
);
}
}
81 changes: 81 additions & 0 deletions lib/paginate.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import * as faker from 'faker';
import { Got, HTTPError, Response } from 'got';
import { Test, TestingModule } from '@nestjs/testing';

import { GotService } from './got.service';
import { GOT_INSTANCE } from './got.constant';
import { PaginationService } from './paginate.service';

describe('GotService', () => {
let service: PaginationService;
const gotInstance: Partial<Got> = {
defaults: {
options: jest.fn(),
} as any,
},
exemptedKeys = ['makeObservable', 'defaults', 'constructor'];

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
PaginationService,
{
provide: GOT_INSTANCE,
useValue: gotInstance,
},
{
provide: GotService,
useValue: {},
},
],
}).compile();

service = module.get<PaginationService>(PaginationService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});

const methods = Object.getOwnPropertyNames(
PaginationService.prototype,
).filter(key => !~exemptedKeys.indexOf(key));

methods.forEach((key, index) => {
it(`${key}()`, complete => {
const result: Partial<Response> = { body: {} };

gotInstance[key] = jest.fn().mockResolvedValueOnce(result);

service[key](faker.internet.url()).subscribe({
next(response) {
expect(response).toBe(result);
},
complete,
});
});

if (methods.length - 2 === index) {
it('check that defaults is set', () =>
expect('options' in service.defaults).toBe(true));

it('should get request', () => {
service[key](faker.internet.url());
});

it('should check error reporting', () => {
const result: any = { body: {}, statusCode: 400 };

gotInstance[key] = jest
.fn()
.mockRejectedValueOnce(new HTTPError(result));

service[key](faker.internet.url()).subscribe({
error(error) {
expect(error).toBeInstanceOf(HTTPError);
},
});
});
}
});
});
Loading

0 comments on commit 12a7df5

Please sign in to comment.