-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
259 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import { Channel } from './channel' | ||
import { | ||
EntryListAPI, | ||
OnEntryListUpdatedHandler, | ||
OnEntryListUpdatedHandlerProps, | ||
OnEntryListUpdatedHandlerReturn, | ||
} from './types' | ||
|
||
interface Message { | ||
msgId: string | ||
props: OnEntryListUpdatedHandlerProps | ||
} | ||
|
||
export default function createEntryList(channel: Channel): EntryListAPI { | ||
let _handler: OnEntryListUpdatedHandler | undefined | ||
let cachedMessage: Message | undefined | ||
|
||
const entryListUpdatedHandler = async (message: Message) => { | ||
cachedMessage = message | ||
|
||
if (!_handler) { | ||
return | ||
} | ||
|
||
const result = await runHandler(_handler, message.props) | ||
return channel.send('entryListResult', { msgId: message.msgId, result }) | ||
} | ||
|
||
channel.addHandler('entryListUpdated', entryListUpdatedHandler) | ||
|
||
return { | ||
onEntryListUpdated(handler: OnEntryListUpdatedHandler) { | ||
if (typeof handler !== 'function') { | ||
throw new Error('OnEntryListUpdated handler must be a function') | ||
} | ||
|
||
_handler = handler | ||
if (cachedMessage) { | ||
entryListUpdatedHandler(cachedMessage) | ||
} | ||
}, | ||
} | ||
} | ||
|
||
const runHandler = async ( | ||
handler: OnEntryListUpdatedHandler, | ||
handlerArg: OnEntryListUpdatedHandlerProps | ||
) => { | ||
try { | ||
// await will accept both async and sync functions, no need for the isPromise check | ||
const result = await handler(handlerArg) | ||
validateResult(result) | ||
|
||
return result | ||
} catch (error) { | ||
console.error(error) | ||
return false | ||
} | ||
} | ||
|
||
const validateResult = (result: OnEntryListUpdatedHandlerReturn) => { | ||
if (result === false) { | ||
return | ||
} | ||
|
||
if (typeof result === 'object') { | ||
validateData(result) | ||
return | ||
} | ||
|
||
throw new Error(`EntryListResult is invalid.`) | ||
} | ||
|
||
const schema: Record<string, (value: unknown) => boolean> = { | ||
values: (value) => | ||
typeof value === 'object' && | ||
Object.keys(value as Record<string, unknown>).length > 0 && | ||
Object.values(value as Record<string, unknown>).every((item) => typeof item === 'string'), | ||
} | ||
|
||
const validateData = (data: Record<string, unknown>) => { | ||
const dataKeys = Object.keys(data) | ||
|
||
if (dataKeys.length === 0) { | ||
throw new Error(`EntryListResult data is invalid.`) | ||
} | ||
|
||
dataKeys.forEach((key: string) => { | ||
if (!(key in schema)) { | ||
throw new Error(`EntryListResult data is invalid. Key "${key}" is not allowed.`) | ||
} | ||
|
||
if (!schema[key](data[key])) { | ||
throw new Error(`EntryListResult data is invalid. Invalid value of key "${key}."`) | ||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { EntryProps } from 'contentful-management/types' | ||
|
||
export type EntryListEntriesType = EntryProps[] | ||
|
||
export type EntryListExtraDataType = { | ||
values: { [entryId: string]: string } | ||
} | ||
|
||
export type OnEntryListUpdatedHandlerReturn = EntryListExtraDataType | false | ||
|
||
export type OnEntryListUpdatedHandlerProps = { | ||
entries: EntryListEntriesType | ||
} | ||
|
||
export type OnEntryListUpdatedHandler = ( | ||
props: OnEntryListUpdatedHandlerProps | ||
) => OnEntryListUpdatedHandlerReturn | Promise<OnEntryListUpdatedHandlerReturn> | ||
|
||
export interface EntryListAPI { | ||
/** Registers a handler to be called every time entries on EntryList changes */ | ||
onEntryListUpdated: (handler: OnEntryListUpdatedHandler) => void | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import { sinon, expect } from '../helpers' | ||
|
||
import createEntryList from '../../lib/entry-list' | ||
import { | ||
EntryListAPI, | ||
OnEntryListUpdatedHandler, | ||
OnEntryListUpdatedHandlerReturn, | ||
} from '../../lib/types' | ||
|
||
const DATA_STUB: OnEntryListUpdatedHandlerReturn = { | ||
values: { | ||
entryId: 'value', | ||
}, | ||
} | ||
|
||
describe('createEntryList()', () => { | ||
describe('returned "entryList" object', () => { | ||
let channelStub: any | ||
let entryList: EntryListAPI | ||
|
||
beforeEach(() => { | ||
channelStub = { addHandler: sinon.spy(), send: sinon.spy() } | ||
entryList = createEntryList(channelStub) | ||
}) | ||
|
||
describe('.onEntryListUpdated()', () => { | ||
const test = async ( | ||
handler: OnEntryListUpdatedHandler | undefined, | ||
result: OnEntryListUpdatedHandlerReturn | ||
) => { | ||
expect(channelStub.addHandler).to.have.been.calledOnce // eslint-disable-line no-unused-expressions | ||
const [channelMethod, sendMessage] = channelStub.addHandler.args[0] | ||
expect(channelMethod).to.eql('entryListUpdated') | ||
|
||
if (handler) { | ||
entryList.onEntryListUpdated(handler) | ||
} | ||
const msgId = 'testId' | ||
const msg = { msgId, props: {} } | ||
const expected = { msgId, result } | ||
|
||
await sendMessage(msg) | ||
|
||
expect(channelStub.send).to.have.been.calledWithExactly('entryListResult', expected) | ||
} | ||
|
||
it('requires the handler to be a function', () => { | ||
expect(() => { | ||
entryList.onEntryListUpdated('wrong handler' as any) | ||
}).to.throw(/OnEntryListUpdated handler must be a function/) | ||
}) | ||
|
||
it('will only call the last added handler', () => { | ||
const first = sinon.spy() | ||
const second = sinon.spy() | ||
|
||
entryList.onEntryListUpdated(first) | ||
entryList.onEntryListUpdated(second) | ||
|
||
const [, sendMessage] = channelStub.addHandler.args[0] | ||
sendMessage({ msgId: 'testId', props: {} }) | ||
|
||
sinon.assert.notCalled(first) | ||
sinon.assert.calledOnce(second) | ||
}) | ||
|
||
it('returns result when handler is sync function', () => test(() => DATA_STUB, DATA_STUB)) | ||
|
||
it('returns result when handler is async function', () => | ||
test(async () => DATA_STUB, DATA_STUB)) | ||
|
||
it('returns false when an error is thrown', () => | ||
test(async () => { | ||
throw new Error() | ||
}, false)) | ||
|
||
it('returns false when a promise rejects', () => | ||
test(() => Promise.reject(new Error()), false)) | ||
|
||
it('returns false if the result data has invalid key', () => | ||
test( | ||
() => | ||
({ | ||
wrongKey: { | ||
entryId: 'value', | ||
}, | ||
} as any), | ||
false | ||
)) | ||
|
||
it('returns false if the result data has an invalid value type', () => | ||
test( | ||
() => ({ | ||
values: { | ||
entryId: {} as any, | ||
}, | ||
}), | ||
false | ||
)) | ||
|
||
it('returns false if the result data has empty values', () => | ||
test( | ||
() => ({ | ||
values: {}, | ||
}), | ||
false | ||
)) | ||
}) | ||
}) | ||
}) |