diff --git a/docs/facade.md b/docs/facade.md index b3ec98cd..7192b30f 100644 --- a/docs/facade.md +++ b/docs/facade.md @@ -14,8 +14,8 @@ The facade contains common functions for storage and retrieval of entities from The functions have some common options that they use. -- [Entity](./options.md#entity) - [Id](./options.md#id) +- [Entity](./options.md#entity) - [Patch](./options.md#patch) - [Filter](./options.md#filter) - [Sort](./options.md#sort) diff --git a/docs/functions.md b/docs/functions.md index bf54306b..0d39f143 100644 --- a/docs/functions.md +++ b/docs/functions.md @@ -28,7 +28,7 @@ Creates a new entity using the `entity` option if no entity exists that matches ```ts const { entity } = await facade.createEntity({ - id: { id: 'example_id' }, + id: 'example_id', entity: { id: 'example_id', foo: 'bar' }, }); ``` @@ -63,7 +63,7 @@ Retrieves a single entity that matches the `id` option. ```ts const { entity } = await facade.getEntity({ - id: { id: 'example_id' }, + id: 'example_id', }); ``` @@ -74,7 +74,7 @@ For an entity that matches the `id` option, it changes all of an entity's proper ```ts const { entity } = await facade.overwriteEntity({ - id: { id: 'example_id' }, + id: 'example_id', entity: { id: 'example_id', foo: 'bar' }, }); ``` @@ -86,7 +86,7 @@ For an entity that matches the `id` option, it changes some of an entity's prope ```ts const { entity } = await facade.patchEntity({ - id: { id: 'example_id' }, + id: 'example_id', patch: { foo: 'bar' }, }); ``` @@ -109,7 +109,7 @@ Removes an entity that matches the `id` option. ```ts await facade.removeEntity({ - id: { id: 'example_id' }, + id: 'example_id', }); ``` @@ -120,9 +120,9 @@ Creates an entity when no entity exists that matches the `id` option. Otherwise, ```ts await facade.upsertEntity({ - id: { id: 'example_id' }, + id: 'example_id', entity: { id: 'example_id', foo: 'bar' }, }); ``` -This package contains the [upsert entity tests](../src/tests/upsertsEntity) and the [upsert entity signature](../src/signatures/UpsertEntity.ts) for this function. \ No newline at end of file +This package contains the [upsert entity tests](../src/tests/upsertsEntity) and the [upsert entity signature](../src/signatures/UpsertEntity.ts) for this function. diff --git a/docs/options.md b/docs/options.md index 029a5203..5c6ed6ba 100644 --- a/docs/options.md +++ b/docs/options.md @@ -10,23 +10,17 @@ The [facade](./facade.md) [functions](./functions.md) have some common options t - [Pagination](#pagination) ### Id -This is an object that contains only the properties required to distinctly identify an entity. In most common cases there is a single property making the [unique key](https://en.wikipedia.org/wiki/Unique_key) so it will likely just contain the `id` property. However, for entities with multiple properties making the unique key it will contain those properties. - -This interface is user-defined hence not contained in this package, the interface below demonstrates what this will look like in most cases. - -```ts -interface Id { - readonly id: string; -} -``` +This is a string that uniquely identifies an entity. ### Entity This is an object that contains all of the entity's properties. The word "entity" has been borrowed from [Entity-Relationship Models/Diagrams](https://en.wikipedia.org/wiki/Entity%E2%80%93relationship_model) and has been used instead of the word "model" to avoid confusion with MVC. -This interface is user-defined hence not contained in this package, the interface below demonstrates what this might look like for a todo entity and extends the [Id interface from the Id example](#id). +This interface is user-defined hence not contained in this package, the interface below demonstrates what this might look like for a todo entity and extends the [TypeScript Entity interface](../src/types/Entity.ts) defined in this package which contains the `id` property. ```ts -interface TodoEntity extends Id { +import Entity from '@js-entity-repos/core/dist/types/Entity'; + +interface TodoEntity extends Entity { readonly description: string; readonly completed: boolean; } @@ -44,8 +38,8 @@ This is an object where a key represents the entity property to be sorted and th This package contains the [TypeScript Sort type definition](../src/types/Sort.ts). ### Pagination -This is an object with three properties, `limit`, `forward`, and `cursor`. The `limit` property defines how many entities to return (maximum). The `forward` property defines whether the entities should be iterate through the entities forwards (when `true`) or backwards (when `false`) from the `cursor`. The `cursor` property defines where to start iterating through the entities. +This is an object with three properties, `limit`, `forward`, and `cursor`. The `limit` property defines how many entities to return (maximum). The `forward` property defines whether the entities should be iterated through forwards (when `true`) or backwards (when `false`) from the `cursor`. The `cursor` property defines where to start iterating through the entities. Cursors have been used instead of `skip` and `limit` to avoid the [pagination issues discussed by Rakhitha Nimesh](https://www.sitepoint.com/paginating-real-time-data-cursor-based-pagination/). -Concrete implementations of the facade can use the [`createCursorFromEntity`](../src/utils/createCursorFromEntity) and [`createPaginationFilter`](../src/utils/createPaginationFilter) util functions to generate cursors. +Concrete implementations of the facade can use the [`createCursorFromEntity`](../src/utils/createCursorFromEntity) and [`createPaginationFilter`](../src/utils/createPaginationFilter) utility functions to generate cursors. This package also contains the [TypeScript Pagination interface](../src/types/Pagination.ts) and the [TypeScript Cursor type definition](../src/types/Cursor.ts). \ No newline at end of file diff --git a/readme.md b/readme.md index d13d7f1f..78427664 100644 --- a/readme.md +++ b/readme.md @@ -4,3 +4,13 @@ ### Usage - Install it with `npm i @js-entity-repos/core`. - Understand it by reading the [documenation](./docs/facade.md). + +### FeathersJS +The project has some similarities with parts of the [FeathersJS framework](feathersjs.com), but unfortunately I've found the following issues with FeathersJS. + +- Their pagination uses skip and limit instead of cursors which causes [issues as discussed by Rakhitha Nimesh](https://www.sitepoint.com/paginating-real-time-data-cursor-based-pagination/). +- Their service interface is missing some functions provided in the [Facade](./docs/facade.md) here. +- Their errors take messages instead of parameters making it harder to support localisation. + +### Thanks +Thanks to [James](https://github.com/ht2), [Mariusz](https://github.com/mariocoski), and [Pete](https://github.com/ee0pdt) at [HT2 Labs](https://www.ht2labs.com) for their feedback. diff --git a/src/Facade.ts b/src/Facade.ts index 8c18bfb0..ba29a28f 100644 --- a/src/Facade.ts +++ b/src/Facade.ts @@ -7,15 +7,16 @@ import PatchEntity from './signatures/PatchEntity'; import RemoveEntities from './signatures/RemoveEntities'; import RemoveEntity from './signatures/RemoveEntity'; import UpsertEntity from './signatures/UpsertEntity'; +import Entity from './types/Entity'; -export default interface Facade { - readonly getEntity: GetEntity; - readonly createEntity: CreateEntity; - readonly overwriteEntity: OverwriteEntity; - readonly patchEntity: PatchEntity; - readonly removeEntity: RemoveEntity; - readonly getEntities: GetEntities; - readonly countEntities: CountEntities; - readonly removeEntities: RemoveEntities; - readonly upsertEntity: UpsertEntity; +export default interface Facade { + readonly getEntity: GetEntity; + readonly createEntity: CreateEntity; + readonly overwriteEntity: OverwriteEntity; + readonly patchEntity: PatchEntity; + readonly removeEntity: RemoveEntity; + readonly getEntities: GetEntities; + readonly countEntities: CountEntities; + readonly removeEntities: RemoveEntities; + readonly upsertEntity: UpsertEntity; } diff --git a/src/errors/ConflictingEntityError.ts b/src/errors/ConflictingEntityError.ts index 49a4fc11..7a0d6301 100644 --- a/src/errors/ConflictingEntityError.ts +++ b/src/errors/ConflictingEntityError.ts @@ -1,8 +1,8 @@ // tslint:disable:no-class import { BaseError } from 'make-error'; -export default class ConflictingEntityError extends BaseError { - constructor(public entityName: string, public entityId: Id) { +export default class ConflictingEntityError extends BaseError { + constructor(public entityName: string, public entityId: string) { super(); } } diff --git a/src/errors/MissingEntityError.ts b/src/errors/MissingEntityError.ts index d45e16da..d39dd9e6 100644 --- a/src/errors/MissingEntityError.ts +++ b/src/errors/MissingEntityError.ts @@ -1,8 +1,8 @@ // tslint:disable:no-class import { BaseError } from 'make-error'; -export default class MissingEntityError extends BaseError { - constructor(public entityName: string, public entityId: Id) { +export default class MissingEntityError extends BaseError { + constructor(public entityName: string, public entityId: string) { super(); } } diff --git a/src/signatures/CountEntities.ts b/src/signatures/CountEntities.ts index bf9e458d..d89803f9 100644 --- a/src/signatures/CountEntities.ts +++ b/src/signatures/CountEntities.ts @@ -1,13 +1,14 @@ +import Entity from '../types/Entity'; import Filter from '../types/Filter'; -export interface Opts { - readonly filter: Filter; +export interface Opts { + readonly filter: Filter; } export interface Result { readonly count: number; } -export type Signature = (opts: Opts) => Promise; +export type Signature = (opts: Opts) => Promise; export default Signature; diff --git a/src/signatures/CreateEntity.ts b/src/signatures/CreateEntity.ts index 17383487..77af05ba 100644 --- a/src/signatures/CreateEntity.ts +++ b/src/signatures/CreateEntity.ts @@ -1,13 +1,14 @@ -export interface Opts { - readonly id: Id; - readonly entity: Entity; +import Entity from '../types/Entity'; + +export interface Opts { + readonly id: string; + readonly entity: E; } -export interface Result { - readonly entity: Entity; +export interface Result { + readonly entity: E; } -export type Signature = - (opts: Opts) => Promise>; +export type Signature = (opts: Opts) => Promise>; export default Signature; diff --git a/src/signatures/GetEntities.ts b/src/signatures/GetEntities.ts index 2d28e1ed..3c011e57 100644 --- a/src/signatures/GetEntities.ts +++ b/src/signatures/GetEntities.ts @@ -1,20 +1,21 @@ import Cursor from '../types/Cursor'; +import Entity from '../types/Entity'; import Filter from '../types/Filter'; import Pagination from '../types/Pagination'; import Sort from '../types/Sort'; -export interface Opts { - readonly filter: Filter; - readonly sort: Sort; +export interface Opts { + readonly filter: Filter; + readonly sort: Sort; readonly pagination: Pagination; } -export interface Result { - readonly entities: Entity[]; +export interface Result { + readonly entities: E[]; readonly nextCursor: Cursor; readonly previousCursor: Cursor; } -export type Signature = (opts: Opts) => Promise>; +export type Signature = (opts: Opts) => Promise>; export default Signature; diff --git a/src/signatures/GetEntity.ts b/src/signatures/GetEntity.ts index da813651..2106e957 100644 --- a/src/signatures/GetEntity.ts +++ b/src/signatures/GetEntity.ts @@ -1,11 +1,13 @@ -export interface Opts { - readonly id: Id; +import Entity from '../types/Entity'; + +export interface Opts { + readonly id: string; } -export interface Result { - readonly entity: Entity; +export interface Result { + readonly entity: E; } -export type Signature = (opts: Opts) => Promise>; +export type Signature = (opts: Opts) => Promise>; export default Signature; diff --git a/src/signatures/OverwriteEntity.ts b/src/signatures/OverwriteEntity.ts index 2872619f..77af05ba 100644 --- a/src/signatures/OverwriteEntity.ts +++ b/src/signatures/OverwriteEntity.ts @@ -1,13 +1,14 @@ -export interface Opts { - readonly id: Id; - readonly entity: Entity; +import Entity from '../types/Entity'; + +export interface Opts { + readonly id: string; + readonly entity: E; } -export interface Result { - readonly entity: Entity; +export interface Result { + readonly entity: E; } -export type Signature = - (opts: Opts) => Promise>; +export type Signature = (opts: Opts) => Promise>; export default Signature; diff --git a/src/signatures/PatchEntity.ts b/src/signatures/PatchEntity.ts index 69db96d7..8b74d5a6 100644 --- a/src/signatures/PatchEntity.ts +++ b/src/signatures/PatchEntity.ts @@ -1,13 +1,14 @@ -export interface Opts { - readonly id: Id; - readonly patch: Partial; +import Entity from '../types/Entity'; + +export interface Opts { + readonly id: string; + readonly patch: Partial; } -export interface Result { - readonly entity: Entity; +export interface Result { + readonly entity: E; } -export type Signature = - (opts: Opts) => Promise>; +export type Signature = (opts: Opts) => Promise>; export default Signature; diff --git a/src/signatures/RemoveEntities.ts b/src/signatures/RemoveEntities.ts index 7bb1e54b..a6dece1e 100644 --- a/src/signatures/RemoveEntities.ts +++ b/src/signatures/RemoveEntities.ts @@ -1,11 +1,12 @@ +import Entity from '../types/Entity'; import Filter from '../types/Filter'; -export interface Opts { - readonly filter: Filter; +export interface Opts { + readonly filter: Filter; } export type Result = void; -export type Signature = (opts: Opts) => Promise; +export type Signature = (opts: Opts) => Promise; export default Signature; diff --git a/src/signatures/RemoveEntity.ts b/src/signatures/RemoveEntity.ts index 29a7c6e4..ae9701e9 100644 --- a/src/signatures/RemoveEntity.ts +++ b/src/signatures/RemoveEntity.ts @@ -1,9 +1,9 @@ -export interface Opts { - readonly id: Id; +export interface Opts { + readonly id: string; } export type Result = void; -export type Signature = (opts: Opts) => Promise; +export type Signature = (opts: Opts) => Promise; export default Signature; diff --git a/src/signatures/UpsertEntity.ts b/src/signatures/UpsertEntity.ts index 2872619f..77af05ba 100644 --- a/src/signatures/UpsertEntity.ts +++ b/src/signatures/UpsertEntity.ts @@ -1,13 +1,14 @@ -export interface Opts { - readonly id: Id; - readonly entity: Entity; +import Entity from '../types/Entity'; + +export interface Opts { + readonly id: string; + readonly entity: E; } -export interface Result { - readonly entity: Entity; +export interface Result { + readonly entity: E; } -export type Signature = - (opts: Opts) => Promise>; +export type Signature = (opts: Opts) => Promise>; export default Signature; diff --git a/src/tests/countEntities/test.ts b/src/tests/countEntities/test.ts index 5077f6ec..f4d33623 100644 --- a/src/tests/countEntities/test.ts +++ b/src/tests/countEntities/test.ts @@ -2,9 +2,9 @@ import 'mocha'; // tslint:disable-line:no-import-side-effect import * as assert from 'power-assert'; import Facade from '../../Facade'; import filterTest from '../utils/filterTest'; -import { TestEntity, TestId } from '../utils/testEntity'; +import { TestEntity } from '../utils/testEntity'; -export default (facade: Facade) => { +export default (facade: Facade) => { describe('countEntities', () => { filterTest({ assertAllEntitiesFilter: async (filter) => { diff --git a/src/tests/createEntity/test.ts b/src/tests/createEntity/test.ts index 9ce5c401..6a8656f5 100644 --- a/src/tests/createEntity/test.ts +++ b/src/tests/createEntity/test.ts @@ -2,9 +2,9 @@ import * as assertRejects from 'assert-rejects'; import 'mocha'; // tslint:disable-line:no-import-side-effect import ConflictingEntityError from '../../errors/ConflictingEntityError'; import Facade from '../../Facade'; -import { TestEntity, testEntity, testId, TestId } from '../utils/testEntity'; +import { TestEntity, testEntity, testId } from '../utils/testEntity'; -export default (facade: Facade) => { +export default (facade: Facade) => { describe('createEntity', () => { it('should not error when identifier does not exist', async () => { await facade.createEntity({ id: testId, entity: testEntity }); diff --git a/src/tests/getEntities/filterTest.ts b/src/tests/getEntities/filterTest.ts index 50fbb25a..8923e2a2 100644 --- a/src/tests/getEntities/filterTest.ts +++ b/src/tests/getEntities/filterTest.ts @@ -4,9 +4,9 @@ import Facade from '../../Facade'; import Pagination from '../../types/Pagination'; import Sort from '../../types/Sort'; import filterTest, { firstEntity, secondEntity } from '../utils/filterTest'; -import { TestEntity, TestId } from '../utils/testEntity'; +import { TestEntity } from '../utils/testEntity'; -export default (facade: Facade) => { +export default (facade: Facade) => { const sort: Sort = {}; const pagination: Pagination = { cursor: undefined, forward: true, limit: 2 }; diff --git a/src/tests/getEntities/paginationTest.ts b/src/tests/getEntities/paginationTest.ts index 68e58972..6cbc9124 100644 --- a/src/tests/getEntities/paginationTest.ts +++ b/src/tests/getEntities/paginationTest.ts @@ -5,13 +5,13 @@ import Cursor from '../../types/Cursor'; import Filter from '../../types/Filter'; import Pagination from '../../types/Pagination'; import Sort from '../../types/Sort'; -import { TestEntity, testEntity, TestId } from '../utils/testEntity'; +import { TestEntity, testEntity } from '../utils/testEntity'; -export default (facade: Facade) => { - const firstId = { id: 'test_id_1' }; - const secondId = { id: 'test_id_2' }; - const firstEntity = { ...testEntity, ...firstId }; - const secondEntity = { ...testEntity, ...secondId }; +export default (facade: Facade) => { + const firstId = 'test_id_1'; + const secondId = 'test_id_2'; + const firstEntity = { ...testEntity, id: firstId }; + const secondEntity = { ...testEntity, id: secondId }; const sort: Sort = { id: true }; const filter: Filter = {}; diff --git a/src/tests/getEntities/sortTest.ts b/src/tests/getEntities/sortTest.ts index 8b9fbd0c..db10795f 100644 --- a/src/tests/getEntities/sortTest.ts +++ b/src/tests/getEntities/sortTest.ts @@ -4,13 +4,13 @@ import Facade from '../../Facade'; import Filter from '../../types/Filter'; import Pagination from '../../types/Pagination'; import Sort from '../../types/Sort'; -import { TestEntity, testEntity, TestId } from '../utils/testEntity'; +import { TestEntity, testEntity } from '../utils/testEntity'; -export default (facade: Facade) => { - const firstId = { id: 'test_id_1' }; - const secondId = { id: 'test_id_2' }; - const firstEntity = { ...testEntity, ...firstId, stringProp: 'a', numberProp: 1 }; - const secondEntity = { ...testEntity, ...secondId, stringProp: 'b', numberProp: 2 }; +export default (facade: Facade) => { + const firstId = 'test_id_1'; + const secondId = 'test_id_2'; + const firstEntity = { ...testEntity, id: firstId, stringProp: 'a', numberProp: 1 }; + const secondEntity = { ...testEntity, id: secondId, stringProp: 'b', numberProp: 2 }; const assertSort = async (sortedEntities: TestEntity[], sort: Sort) => { const filter: Filter = {}; diff --git a/src/tests/getEntities/test.ts b/src/tests/getEntities/test.ts index 8733f8ca..4d045d78 100644 --- a/src/tests/getEntities/test.ts +++ b/src/tests/getEntities/test.ts @@ -1,11 +1,11 @@ import 'mocha'; // tslint:disable-line:no-import-side-effect import Facade from '../../Facade'; -import { TestEntity, TestId } from '../utils/testEntity'; +import { TestEntity } from '../utils/testEntity'; import filterTest from './filterTest'; import paginationTest from './paginationTest'; import sortTest from './sortTest'; -export default (facade: Facade) => { +export default (facade: Facade) => { describe('getEntities', () => { filterTest(facade); paginationTest(facade); diff --git a/src/tests/getEntity/test.ts b/src/tests/getEntity/test.ts index 23c0b814..92a9e077 100644 --- a/src/tests/getEntity/test.ts +++ b/src/tests/getEntity/test.ts @@ -3,9 +3,9 @@ import 'mocha'; // tslint:disable-line:no-import-side-effect import * as assert from 'power-assert'; import MissingEntityError from '../../errors/MissingEntityError'; import Facade from '../../Facade'; -import { TestEntity, testEntity, testId, TestId } from '../utils/testEntity'; +import { TestEntity, testEntity, testId } from '../utils/testEntity'; -export default (facade: Facade) => { +export default (facade: Facade) => { describe('getEntity', () => { it('should error when identifier does not exist', async () => { const promise = facade.getEntity({ id: testId }); diff --git a/src/tests/index.ts b/src/tests/index.ts index bee988d3..497822ab 100644 --- a/src/tests/index.ts +++ b/src/tests/index.ts @@ -9,9 +9,9 @@ import patchEntityTest from './patchEntity/test'; import removeEntitiesTest from './removeEntities/test'; import removeEntityTest from './removeEntity/test'; import upsertEntityTest from './upsertEntity/test'; -import { TestEntity, TestId } from './utils/testEntity'; +import { TestEntity } from './utils/testEntity'; -export default (facade: Facade) => { +export default (facade: Facade) => { describe('facade', () => { beforeEach(async () => { await facade.removeEntities({ filter: {} }); diff --git a/src/tests/overwriteEntity/test.ts b/src/tests/overwriteEntity/test.ts index 54c90f79..763fc5fb 100644 --- a/src/tests/overwriteEntity/test.ts +++ b/src/tests/overwriteEntity/test.ts @@ -3,9 +3,9 @@ import 'mocha'; // tslint:disable-line:no-import-side-effect import * as assert from 'power-assert'; import MissingEntityError from '../../errors/MissingEntityError'; import Facade from '../../Facade'; -import { TestEntity, testEntity, testId, TestId } from '../utils/testEntity'; +import { TestEntity, testEntity, testId } from '../utils/testEntity'; -export default (facade: Facade) => { +export default (facade: Facade) => { describe('overwriteEntity', () => { it('should error when identifier does not exist', async () => { const promise = facade.overwriteEntity({ id: testId, entity: testEntity }); @@ -14,8 +14,8 @@ export default (facade: Facade) => { it('should overwrite when identifier does exist', async () => { const testOverwrite: TestEntity = { - ...testId, booleanProp: false, + id: testId, numberProp: 2, stringProp: 'test_string_prop_overwrite', }; diff --git a/src/tests/patchEntity/test.ts b/src/tests/patchEntity/test.ts index 92df7b89..b74a9bbc 100644 --- a/src/tests/patchEntity/test.ts +++ b/src/tests/patchEntity/test.ts @@ -3,9 +3,9 @@ import 'mocha'; // tslint:disable-line:no-import-side-effect import * as assert from 'power-assert'; import MissingEntityError from '../../errors/MissingEntityError'; import Facade from '../../Facade'; -import { TestEntity, testEntity, testId, TestId } from '../utils/testEntity'; +import { TestEntity, testEntity, testId } from '../utils/testEntity'; -export default (facade: Facade) => { +export default (facade: Facade) => { describe('patchEntity', () => { const assertPatch = async (testPatch: Partial) => { const { entity: patchedEntity } = await facade.patchEntity({ diff --git a/src/tests/removeEntities/test.ts b/src/tests/removeEntities/test.ts index 43edea1a..f6afc17f 100644 --- a/src/tests/removeEntities/test.ts +++ b/src/tests/removeEntities/test.ts @@ -4,9 +4,9 @@ import Facade from '../../Facade'; import Pagination from '../../types/Pagination'; import Sort from '../../types/Sort'; import filterTest, { firstEntity, secondEntity } from '../utils/filterTest'; -import { TestEntity, TestId } from '../utils/testEntity'; +import { TestEntity } from '../utils/testEntity'; -export default (facade: Facade) => { +export default (facade: Facade) => { describe('removeEntities', () => { const sort: Sort = {}; const pagination: Pagination = { cursor: undefined, forward: true, limit: 2 }; diff --git a/src/tests/removeEntity/test.ts b/src/tests/removeEntity/test.ts index e23d8427..ab872e7e 100644 --- a/src/tests/removeEntity/test.ts +++ b/src/tests/removeEntity/test.ts @@ -2,9 +2,9 @@ import * as assertRejects from 'assert-rejects'; import 'mocha'; // tslint:disable-line:no-import-side-effect import MissingEntityError from '../../errors/MissingEntityError'; import Facade from '../../Facade'; -import { TestEntity, testEntity, testId, TestId } from '../utils/testEntity'; +import { TestEntity, testEntity, testId } from '../utils/testEntity'; -export default (facade: Facade) => { +export default (facade: Facade) => { describe('removeEntity', () => { it('should error when identifier does not exist', async () => { const promise = facade.removeEntity({ id: testId }); diff --git a/src/tests/upsertEntity/test.ts b/src/tests/upsertEntity/test.ts index e4254f86..a11cf6f0 100644 --- a/src/tests/upsertEntity/test.ts +++ b/src/tests/upsertEntity/test.ts @@ -1,9 +1,9 @@ import 'mocha'; // tslint:disable-line:no-import-side-effect import * as assert from 'power-assert'; import Facade from '../../Facade'; -import { TestEntity, testEntity, testId, TestId } from '../utils/testEntity'; +import { TestEntity, testEntity, testId } from '../utils/testEntity'; -export default (facade: Facade) => { +export default (facade: Facade) => { describe('upsertEntity', () => { it('should create when identifier does not exist', async () => { const { entity: createdEntity } = await facade.upsertEntity({ @@ -17,8 +17,8 @@ export default (facade: Facade) => { it('should overwrite when identifier does exist', async () => { const testOverwrite: TestEntity = { - ...testId, booleanProp: false, + id: testId, numberProp: 2, stringProp: 'test_string_prop_overwrite', }; diff --git a/src/tests/utils/filterTest.ts b/src/tests/utils/filterTest.ts index 99b8fcc0..d00f0e45 100644 --- a/src/tests/utils/filterTest.ts +++ b/src/tests/utils/filterTest.ts @@ -1,20 +1,20 @@ import 'mocha'; // tslint:disable-line:no-import-side-effect import Facade from '../../Facade'; import Filter from '../../types/Filter'; -import { TestEntity, testEntity, TestId } from '../utils/testEntity'; +import { TestEntity, testEntity } from '../utils/testEntity'; export type FilterAsserter = (filter: Filter) => Promise; export interface Opts { - readonly facade: Facade; + readonly facade: Facade; readonly assertFirstEntityFilter: FilterAsserter; readonly assertNoEntityFilter: FilterAsserter; readonly assertAllEntitiesFilter: FilterAsserter; } -const firstId = { id: 'test_id_1' }; -const secondId = { id: 'test_id_2' }; -export const firstEntity = { ...testEntity, ...firstId, stringProp: 'a', numberProp: 1 }; -export const secondEntity = { ...testEntity, ...secondId, stringProp: 'b', numberProp: 2 }; +const firstId = 'test_id_1'; +const secondId = 'test_id_2'; +export const firstEntity = { ...testEntity, id: firstId, stringProp: 'a', numberProp: 1 }; +export const secondEntity = { ...testEntity, id: secondId, stringProp: 'b', numberProp: 2 }; export default (opts: Opts) => { const createTestEntities = async () => { diff --git a/src/tests/utils/testEntity.ts b/src/tests/utils/testEntity.ts index 1b885f2f..c86db6d9 100644 --- a/src/tests/utils/testEntity.ts +++ b/src/tests/utils/testEntity.ts @@ -1,20 +1,16 @@ -export interface TestId { - readonly id: string; -} +import Entity from '../../types/Entity'; -export interface TestEntity extends TestId { +export interface TestEntity extends Entity { readonly stringProp: string; readonly numberProp: number; readonly booleanProp: boolean; } -export const testId: TestId = { - id: 'test_id', -}; +export const testId = 'test_id'; export const testEntity: TestEntity = { - ...testId, booleanProp: true, + id: testId, numberProp: 1, stringProp: 'test_string_prop', }; diff --git a/src/types/Entity.ts b/src/types/Entity.ts new file mode 100644 index 00000000..7df84f63 --- /dev/null +++ b/src/types/Entity.ts @@ -0,0 +1,3 @@ +export default interface Entity { + readonly id: string; +} diff --git a/src/types/Filter.ts b/src/types/Filter.ts index 6c0aab0a..6b0f1558 100644 --- a/src/types/Filter.ts +++ b/src/types/Filter.ts @@ -1,3 +1,5 @@ +import Entity from './Entity'; + export interface PropFilter { readonly $gt?: Prop; readonly $gte?: Prop; @@ -10,19 +12,19 @@ export interface PropFilter { readonly $not?: PropFilter; } -export type EntityFilter = { - readonly [P in keyof Entity]?: Entity[P] | PropFilter; +export type EntityFilter = { + readonly [P in keyof E]?: E[P] | PropFilter; }; -export interface ConditionFilter { - readonly $and?: Filter[]; - readonly $or?: Filter[]; - readonly $nor?: Filter[]; +export interface ConditionFilter { + readonly $and?: Filter[]; + readonly $or?: Filter[]; + readonly $nor?: Filter[]; } -export type Filter = ( - EntityFilter & - ConditionFilter +export type Filter = ( + EntityFilter & + ConditionFilter ); export default Filter; diff --git a/src/types/Sort.ts b/src/types/Sort.ts index e2a33d42..a2f4881e 100644 --- a/src/types/Sort.ts +++ b/src/types/Sort.ts @@ -1,5 +1,7 @@ -type Sort = { - readonly [P in keyof Entity]?: boolean; +import Entity from './Entity'; + +type Sort = { + readonly [P in keyof E]?: boolean; }; export default Sort; diff --git a/src/types/index.ts b/src/types/index.ts deleted file mode 100644 index 938499c8..00000000 --- a/src/types/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import CursorType from './Cursor'; -import FilterType from './Filter'; -import PaginationType from './Pagination'; -import SortType from './Sort'; - -export type Cursor = CursorType; -export type Filter = FilterType; -export type Paginatior = PaginationType; -export type Sort = SortType; diff --git a/src/utils/createCursorFromEntity/index.ts b/src/utils/createCursorFromEntity/index.ts index 941229c6..048aba1a 100644 --- a/src/utils/createCursorFromEntity/index.ts +++ b/src/utils/createCursorFromEntity/index.ts @@ -1,14 +1,15 @@ import * as btoa from 'btoa'; import { get, set } from 'lodash'; import Cursor from '../../types/Cursor'; +import Entity from '../../types/Entity'; import Sort from '../../types/Sort'; -export default (entity: Entity | undefined, sort: Sort): Cursor => { +export default (entity: E | undefined, sort: Sort): Cursor => { if (entity === undefined) { return undefined; } const sortKeys = Object.keys(sort); - const cursorResult = sortKeys.reduce>((result, sortKey) => { + const cursorResult = sortKeys.reduce>((result, sortKey) => { return set(result, sortKey, get(entity, sortKey)); }, {}); return btoa(JSON.stringify(cursorResult)); diff --git a/src/utils/createPaginationFilter/index.test.ts b/src/utils/createPaginationFilter/index.test.ts index 7e020c26..f09117ca 100644 --- a/src/utils/createPaginationFilter/index.test.ts +++ b/src/utils/createPaginationFilter/index.test.ts @@ -1,12 +1,14 @@ import 'mocha'; // tslint:disable-line:no-import-side-effect import * as assert from 'power-assert'; import { TestEntity, testEntity } from '../../tests/utils/testEntity'; +import { Filter } from '../../types/Filter'; import Pagination from '../../types/Pagination'; +import Sort from '../../types/Sort'; import createCursorFromEntity from '../createCursorFromEntity'; import createPaginationFilter from './index'; describe('createCursorFromEntity', () => { - const sort = { id: true }; + const sort: Sort = { id: true, booleanProp: false }; it('should return empty filter when the cursor is undefined', () => { const pagination: Pagination = { cursor: undefined, forward: true, limit: 1 }; @@ -19,7 +21,8 @@ describe('createCursorFromEntity', () => { const cursor = createCursorFromEntity(testEntity, sort); const pagination: Pagination = { cursor, forward: true, limit: 1 }; const actualResult = createPaginationFilter(pagination, sort); - const expectedResult = { + const expectedResult: Filter = { + booleanProp: { $lte: testEntity.booleanProp }, id: { $gt: testEntity.id }, }; assert.deepEqual(actualResult, expectedResult); @@ -29,7 +32,8 @@ describe('createCursorFromEntity', () => { const cursor = createCursorFromEntity(testEntity, sort); const pagination: Pagination = { cursor, forward: false, limit: 1 }; const actualResult = createPaginationFilter(pagination, sort); - const expectedResult = { + const expectedResult: Filter = { + booleanProp: { $gte: testEntity.booleanProp }, id: { $lt: testEntity.id }, }; assert.deepEqual(actualResult, expectedResult); diff --git a/src/utils/createPaginationFilter/index.ts b/src/utils/createPaginationFilter/index.ts index de98d77f..a78060a5 100644 --- a/src/utils/createPaginationFilter/index.ts +++ b/src/utils/createPaginationFilter/index.ts @@ -1,5 +1,6 @@ import * as atob from 'atob'; import { get, mapValues } from 'lodash'; +import Entity from '../../types/Entity'; // tslint:disable-next-line:no-unused import Filter, { ConditionFilter, EntityFilter } from '../../types/Filter'; import Pagination from '../../types/Pagination'; @@ -9,7 +10,7 @@ const xor = (conditionA: boolean, conditionB: boolean) => { return (conditionA && !conditionB) || (!conditionA && conditionB); }; -export default (pagination: Pagination, sort: Sort): Filter => { +export default (pagination: Pagination, sort: Sort): Filter => { if (pagination.cursor === undefined) { return {}; } @@ -17,10 +18,18 @@ export default (pagination: Pagination, sort: Sort): Filter { const forward = !xor(get(sort, sortKey), pagination.forward); if (forward) { - return { $gt: cursorValue }; + if (sortKey === 'id') { + return { $gt: cursorValue }; + } else { + return { $gte: cursorValue }; + } } else { - return { $lt: cursorValue }; + if (sortKey === 'id') { + return { $lt: cursorValue }; + } else { + return { $lte: cursorValue }; + } } }); - return filter as any as Filter; + return filter as any as Filter; };