Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API is now promises only #4

Merged
merged 1 commit into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 15 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,43 +15,22 @@ import { fetchOne, search } from '@abcnews/terminus-fetch';

// By default, we assume you want an Article document from Core Media so you can pass a CMID:

fetchOne(10736062, (err, doc) => {
if (!err) {
console.log(doc);
// > { id: 10736062, docType: "Article", contentSource: "coremedia", ... }
}
});
fetchOne(10736062).then(console.log);
// > { id: 10736062, docType: "Article", contentSource: "coremedia", ... }

// ...or you can pass an options object to override the defaults (see API below):

fetchOne({ id: 10734902, type: 'video' }, (err, doc) => {
if (!err) {
console.log(doc);
// > {id: 10734902, docType: "Video", contentSource: "coremedia", ... }
}
});

// You can use promises instead of callbacks:

fetchOne({ id: 123860, type: 'show', source: 'iview' })
.then(doc => {
console.log(doc);
// > { id: 123860, docType: "show", contentSource: "iview", ... }
})
.catch(err => console.error(err));
fetchOne({ id: 10734902, type: 'video' }).then(console.log);
// > {id: 10734902, docType: "Video", contentSource: "coremedia", ... }

// Searching is also supported:

search({ limit: 3, doctype: 'image' }), (err, docs) => {
if (!err) {
console.log(docs);
// > [
// { id: 11405582, docType: "Image", contentSource: "coremedia", ... },
// { id: 11404970, docType: "Image", contentSource: "coremedia", ... },
// { id: 11405258, docType: "Image", contentSource: "coremedia", ... }
// ]
}
});
search({ limit: 3, doctype: 'image' })).then(console.log);
// > [
// { id: 11405582, docType: "Image", contentSource: "coremedia", ... },
// { id: 11404970, docType: "Image", contentSource: "coremedia", ... },
// { id: 11405258, docType: "Image", contentSource: "coremedia", ... }
// ]

// ...for all sources...:

Expand All @@ -68,7 +47,7 @@ search({ limit: 1, source: 'mapi', service: 'triplej'})
.catch(err => console.error(err));
```

If your project's JS is currently executing in a page on `*.aus.aunty.abc.net.au`, requests will be made to Preview Terminus (`https://api-preview.terminus.abc-prod.net.au/api/v2/{teasable}content`), otherwise they'll be made to Live Terminus (`https://api.abc.net.au/terminus/api/v2/{teasable}content`).
If your project's JS is currently executing in a page on a preview domain, requests will be made to Preview Terminus, otherwise they'll be made to Live Terminus.

If you want to direct a single request to Live Terminus, regardless of the current execution domain, pass `force: "live"` as an option.

Expand All @@ -91,9 +70,8 @@ declare function fetchOne(
id?: string | number;
force?: 'preview' | 'live';
isTeasable?: string;
},
done?: (err?: ProgressEvent | Error, doc?: Object) => void
): void | Promise<Object>;
}
): Promise<TerminusDocument>;
```

If the `done` callback is omitted then the return value will be a Promise.
Expand All @@ -115,9 +93,8 @@ declare function search(
source?: string;
force?: "preview" | "live";
...searchParams: Object;
},
done?: (err?: ProgressEvent | Error, doc?: Object) => void
): void | Promise<Object>;
}
): Promise<TerminusDocument[]>;
```

...where your `searchParams` are additional properties on your `options` object, to query the API.
Expand All @@ -131,8 +108,6 @@ For example, if you wanted the last 20 images added to Core Media, your `searchP
}
```

If the `done` callback is omitted then the return value will be a Promise.

#### Default options

These are the same as `fetchOne`, only split across two options arguments.
Expand Down
3 changes: 3 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@ describe('getImages', () => {
test('Standard image proxy with defaultRatio', () => {
expect(getImages(v2_standard_proxy_with_default_ratio.input).defaultRatio).toEqual('4x3');
});
test('Bad terminus doc', () => {
expect(() => getImages({})).toThrow();
});
});
});
89 changes: 26 additions & 63 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,12 @@ interface SearchOptions extends APIOptions {
source?: string;
[x: string]: unknown;
}
interface TerminusDocument {
export interface TerminusDocument {
_links?: Record<string, unknown>;
_embedded?: {
[key: string]: TerminusDocument[];
};
}
type Callback<E, T> = (err?: E, result?: T) => void;
type Done<T> = Callback<ProgressEvent | Error, T>;

// This built JS asset _will_be_ rewritten on-the-fly, so we need to obscure the origin somewhat
const GENIUNE_MEDIA_ENDPOINT_PATTERN = new RegExp(['http', '://', 'mpegmedia', '.abc.net.au'].join(''), 'g');
Expand Down Expand Up @@ -61,61 +59,38 @@ function getEndpoint(force?: TIERS): string {
: TERMINUS_LIVE_ENDPOINT;
}

function fetchOne(fetchOneOptions: FetchOneOptionsOrDocumentID): Promise<TerminusDocument>;
function fetchOne(fetchOneOptions: FetchOneOptionsOrDocumentID, done: Done<TerminusDocument>): void;
function fetchOne(fetchOneOptions: FetchOneOptionsOrDocumentID, done?: Done<TerminusDocument>): unknown {
return asyncTask(
new Promise<TerminusDocument>((resolve, reject) => {
const { source, type, id, isTeasable, force, version } = {
...DEFAULT_API_OPTIONS,
...DEFAULT_DOCUMENT_OPTIONS,
...ensureIsDocumentOptions(fetchOneOptions)
};
async function fetchOne(fetchOneOptions: FetchOneOptionsOrDocumentID): Promise<TerminusDocument> {
const { source, type, id, isTeasable, force, version } = {
...DEFAULT_API_OPTIONS,
...DEFAULT_DOCUMENT_OPTIONS,
...ensureIsDocumentOptions(fetchOneOptions)
};

if (isDocumentIDInvalid(id as DocumentID)) {
return reject(new Error(`Invalid ID: ${id}`));
}
if (isDocumentIDInvalid(id as DocumentID)) {
throw new Error(`Invalid ID: ${id}`);
}

request(
`${getBaseUrl({ force, version })}/${
isTeasable ? 'teasable' : ''
}content/${source}/${type}/${id}?apikey=${API_KEY}`,
resolve,
reject
);
}),
done
const res = await fetch(
`${getBaseUrl({ force, version })}/${isTeasable ? 'teasable' : ''}content/${source}/${type}/${id}?apikey=${API_KEY}`
);
const responseText = await res.text();
return parse(responseText);
}

function search(searchOptions: SearchOptions): Promise<TerminusDocument[]>;
function search(searchOptions: SearchOptions, done: Done<TerminusDocument[]>): void;
function search(searchOptions?: SearchOptions, done?: Done<TerminusDocument[]>): unknown {
return asyncTask(
new Promise<TerminusDocument[]>((resolve, reject) => {
const { force, source, version, ...searchParams } = {
...DEFAULT_SEARCH_OPTIONS,
...(searchOptions || ({} as SearchOptions))
};
const searchParamsKeys = Object.keys(searchParams);
async function search(searchOptions: SearchOptions): Promise<TerminusDocument[]> {
const { force, source, version, ...searchParams } = {
...DEFAULT_SEARCH_OPTIONS,
...(searchOptions || ({} as SearchOptions))
};
const searchParamsKeys = Object.keys(searchParams);

request(
`${getBaseUrl({ force, version })}/search/${source}?${searchParamsKeys
.map(key => `${key}=${searchParams[key]}`)
.join('&')}${searchParamsKeys.length ? '&' : ''}apikey=${API_KEY}`,
(response: TerminusDocument) => resolve(flattenEmbeddedProps(response._embedded || {})),
reject
);
}),
done
const res = await fetch(
`${getBaseUrl({ force, version })}/search/${source}?${searchParamsKeys
.map(key => `${key}=${searchParams[key]}`)
.join('&')}${searchParamsKeys.length ? '&' : ''}apikey=${API_KEY}`
);
}

// Enable easy support for both promise and callback interfaces
function asyncTask<E, T>(promise: Promise<T>, callback?: Callback<E, T>) {
return callback
? promise.then(result => setTimeout(callback, 0, null, result)).catch(err => setTimeout(callback, 0, err))
: promise;
const _embedded = await res.json();
return flattenEmbeddedProps(_embedded);
}

function ensureIsDocumentOptions(options: DocumentOptionsOrDocumentID): DocumentOptions {
Expand All @@ -126,18 +101,6 @@ function isDocumentIDInvalid(documentID: DocumentID): boolean {
return documentID != +documentID || !String(documentID).length || String(documentID).indexOf('.') > -1;
}

function request(uri: string, resolve: (data: TerminusDocument) => unknown, reject: (err: ProgressEvent) => unknown) {
const xhr = new XMLHttpRequest();
const errorHandler = (event: ProgressEvent) => reject(event);

xhr.onload = event => (xhr.status !== 200 ? reject(event) : resolve(parse(xhr.responseText)));
xhr.onabort = errorHandler;
xhr.onerror = errorHandler;
xhr.open('GET', uri, true);
xhr.responseType = 'text';
xhr.send();
}

function parse(responseText: string): TerminusDocument {
// Terminus is not returning proxied asset URLs (yet)
return JSON.parse(responseText.replace(GENIUNE_MEDIA_ENDPOINT_PATTERN, PROXIED_MEDIA_ENDPOINT));
Expand Down
Loading