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

Commit

Permalink
feat: Added the enumerate API to the trail-core module.
Browse files Browse the repository at this point in the history
  • Loading branch information
Shogun committed May 16, 2018
1 parent e0bed6e commit 2cb1b31
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 8 deletions.
14 changes: 14 additions & 0 deletions packages/trail-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,20 @@ The default sort direction is ascending, but it can be reversed by prepending a

Returns an array of found trail objects.

### `async TrailsManager.enumerate({from, to, type, page, pageSize, desc})`

Searchs for distinct ids in the database.

The `from` and `to` attributes follow the same rule of the `when` trail attributes and are inclusive.

The `type` must be one of the following values: `who`, `what` or `subject`.

The `page` and `pageSize` attributes can be used to control pagination. They must be positive numbers. The default pageSize is 25.

The `desc` can be set to `true` to sort results by descending order.

Returns an array of found id (depending on the `type` attribute), ordered alphabetically.

## License

Copyright nearForm Ltd 2018. Licensed under [MIT][license].
Expand Down
43 changes: 37 additions & 6 deletions packages/trail-core/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,11 @@ class TrailsManager {

async search ({from, to, who, what, subject, page, pageSize, sort} = {}) {
// Validate parameters
if (!from) throw new Error(`You must specify a starting date ("from" attribute) when querying trails.`)
if (!to) throw new Error(`You must specify a ending date ("to" attribute) when querying trails.`)
if (who && typeof who !== 'string') throw new TypeError(`Only strings are supporting for searching in the id of the "who" field.`)
if (what && typeof what !== 'string') throw new TypeError(`Only strings are supporting for searching in the id of the "what" field.`)
if (subject && typeof subject !== 'string') throw new TypeError(`Only strings are supporting for searching in the id of the "subject" field.`)
if (!from) throw new Error('You must specify a starting date ("from" attribute) when querying trails.')
if (!to) throw new Error('You must specify a ending date ("to" attribute) when querying trails.')
if (who && typeof who !== 'string') throw new TypeError('Only strings are supporting for searching in the id of the "who" field.')
if (what && typeof what !== 'string') throw new TypeError('Only strings are supporting for searching in the id of the "what" field.')
if (subject && typeof subject !== 'string') throw new TypeError('Only strings are supporting for searching in the id of the "subject" field.')

from = parseDate(from)
to = parseDate(to)
Expand Down Expand Up @@ -100,6 +100,37 @@ class TrailsManager {
return res.rows.map(convertToTrail)
}

async enumerate ({from, to, type, page, pageSize, desc} = {}) {
// Validate parameters
if (!from) throw new Error('You must specify a starting date ("from" attribute) when enumerating.')
if (!to) throw new Error('You must specify a ending date ("to" attribute) when enumerating.')

from = parseDate(from)
to = parseDate(to)

if (!['who', 'what', 'subject'].includes(type)) throw new TypeError('You must select between "who", "what" or "subject" type ("type" attribute) when enumerating.')

// Sanitize pagination parameters
;({page, pageSize} = this._sanitizePagination(page, pageSize))

// Perform the query
const sql = SQL`
SELECT
DISTINCT ON(${{__raw: type}}_id) ${{__raw: type}}_id AS entry
FROM trails
WHERE
("when" >= ${from.toISO()} AND "when" <= ${to.toISO()})
`

const footer = ` ORDER BY entry ${desc ? 'DESC' : 'ASC'} LIMIT ${pageSize} OFFSET ${(page - 1) * pageSize}`
sql.append(SQL([footer]))

const res = await this.performDatabaseOperations(sql)

return res.rows.map(r => r.entry)
}

async insert (trail) {
trail = convertToTrail(trail)

Expand Down Expand Up @@ -186,7 +217,7 @@ class TrailsManager {
}

if (!['id', 'when', 'who', 'what', 'subject'].includes(sortKey)) {
throw new TypeError(`Only "id", "when", "who", "what" and "subject" are supported for sorting.`)
throw new TypeError('Only "id", "when", "who", "what" and "subject" are supported for sorting.')
}

// Perform some sanitization
Expand Down
4 changes: 2 additions & 2 deletions packages/trail-core/lib/trail.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const parseComponent = function (attributes, id, label) {
const parseDate = function (original) {
if (original instanceof DateTime) return original.setZone('utc')
else if (original instanceof Date) return DateTime.fromMillis(original.getTime(), {zone: 'utc'})
else if (typeof original !== 'string') throw new Error(`Only Luxon DateTime, JavaScript Date or ISO8601 are supported for dates.`)
else if (typeof original !== 'string') throw new Error('Only Luxon DateTime, JavaScript Date or ISO8601 are supported for dates.')

const when = DateTime.fromISO(original)
if (!when.isValid) throw new Error(`Invalid date "${original}". Please specify a valid UTC date in ISO8601 format.`)
Expand All @@ -55,7 +55,7 @@ module.exports = {
parseDate,
convertToTrail ({id, when, who, what, subject, where, why, meta, who_id: whoId, what_id: whatId, subject_id: subjectId}) {
// Convert required fields
if (id !== null && typeof id !== 'undefined' && typeof id !== 'number') throw new Error(`The trail id must be a number or null.`)
if (id !== null && typeof id !== 'undefined' && typeof id !== 'number') throw new Error('The trail id must be a number or null.')

// Return the trail
return {
Expand Down
68 changes: 68 additions & 0 deletions packages/trail-core/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,74 @@ describe('TrailsManager', () => {
})
})

describe('.enumerate', () => {
test('should return the right records', async () => {
await this.subject.performDatabaseOperations(client => client.query('TRUNCATE trails'))

const records = [
{when: '2018-01-01T12:34:56+00:00', who: 'dog', what: 'open', subject: 'window'},
{when: '2018-01-02T12:34:56+00:00', who: 'cat', what: 'open', subject: 'window'},
{when: '2018-01-03T12:34:56+00:00', who: 'whale', what: 'close', subject: 'door'},
{when: '2018-01-04T12:34:56+00:00', who: 'cat', what: 'close', subject: 'door'},
{when: '2018-01-05T12:34:56+00:00', who: 'shark', what: 'check', subject: 'world'}
]

const ids = await Promise.all(records.map(r => this.subject.insert(r)))

expect((await this.subject.enumerate({from: '2018-01-01T11:00:00+00:00', to: '2018-01-04T13:34:56+00:00', type: 'who'})))
.toEqual(['cat', 'dog', 'whale'])

expect((await this.subject.enumerate({from: '2018-01-01T15:00:00+00:00', to: '2018-01-04T13:34:56+00:00', type: 'what'})))
.toEqual(['close', 'open'])

expect((await this.subject.enumerate({from: '2018-01-01T11:00:00+00:00', to: '2018-01-04T13:34:56+00:00', type: 'who', desc: true})))
.toEqual(['whale', 'dog', 'cat'])

expect((await this.subject.enumerate({from: '2018-01-01T15:00:00+00:00', to: '2018-01-05T13:34:56+00:00', type: 'who'})))
.toEqual(['cat', 'shark', 'whale'])

expect((await this.subject.enumerate({from: '2018-01-01T15:00:00+00:00', to: '2018-01-05T13:34:56+00:00', type: 'what'})))
.toEqual(['check', 'close', 'open'])

expect((await this.subject.enumerate({from: '2018-01-01T00:00:00+00:00', to: '2018-01-05T13:34:56+00:00', type: 'who', page: 2, pageSize: 1})))
.toEqual(['dog'])

expect((await this.subject.enumerate({from: '2018-02-01T11:00:00+00:00', to: '2018-02-04T13:34:56+00:00', type: 'who'}))).toEqual([])

await Promise.all(ids.map(i => this.subject.delete(i)))
})

test('should validate parameters', async () => {
await expect(this.subject.enumerate()).rejects.toEqual(new Error('You must specify a starting date ("from" attribute) when enumerating.'))
await expect(this.subject.enumerate({from: DateTime.local()}))
.rejects.toEqual(new Error('You must specify a ending date ("to" attribute) when enumerating.'))

await expect(this.subject.enumerate({from: 'whatever', to: DateTime.local()}))
.rejects.toEqual(new Error('Invalid date "whatever". Please specify a valid UTC date in ISO8601 format.'))
await expect(this.subject.enumerate({from: DateTime.local(), to: DateTime.local(), type: 'foo'}))
.rejects.toEqual(new Error('You must select between "who", "what" or "subject" type ("type" attribute) when enumerating.'))
})

test('should sanitize pagination parameters', async () => {
const spy = jest.spyOn(this.subject, 'performDatabaseOperations')

await this.subject.enumerate({from: DateTime.local(), to: DateTime.local(), type: 'who', page: 12})
expect(spy.mock.calls[0][0].text).toEqual(expect.stringContaining('LIMIT 25 OFFSET 275'))

await this.subject.enumerate({from: DateTime.local(), to: DateTime.local(), type: 'who', pageSize: 12})
expect(spy.mock.calls[1][0].text).toEqual(expect.stringContaining('LIMIT 12 OFFSET 0'))

await this.subject.enumerate({from: DateTime.local(), to: DateTime.local(), type: 'who', page: 3, pageSize: 12})
expect(spy.mock.calls[2][0].text).toEqual(expect.stringContaining('LIMIT 12 OFFSET 24'))

await this.subject.enumerate({from: DateTime.local(), to: DateTime.local(), type: 'who', page: '12', pageSize: NaN})
expect(spy.mock.calls[3][0].text).toEqual(expect.stringContaining('LIMIT 25 OFFSET 275'))

await this.subject.enumerate({from: DateTime.local(), to: DateTime.local(), type: 'who', page: NaN, pageSize: 2})
expect(spy.mock.calls[4][0].text).toEqual(expect.stringContaining('LIMIT 2 OFFSET 0'))
})
})

describe('.insert', () => {
test('should accept a single argument', async () => {
const id = await this.subject.insert(sampleTrail())
Expand Down

0 comments on commit 2cb1b31

Please sign in to comment.