Skip to content

Instantly share code, notes, and snippets.

@brehaut
Last active October 6, 2018 07:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save brehaut/fcf9bc0428f9fb24d7f94b8588838e91 to your computer and use it in GitHub Desktop.
Save brehaut/fcf9bc0428f9fb24d7f94b8588838e91 to your computer and use it in GitHub Desktop.
Types and a couple of utility functions for generating JSONfeeds from node.js and TypeScript
import * as _ from "lodash";
/**
* Types and functions for implementing JSONFeed
*
* Terminolgy and types derived from https://jsonfeed.org/version/1
*/
import * as express from "express";
/**
* FeedVersion are the allowed version strings for feeds
*/
export enum FeedVersion {
V1 = "https://jsonfeed.org/version/1"
}
/**
* IFeedTopLevel is the interface for the object that encapsulates
* the feed metadata and all the entry items
*/
export interface IFeedTopLevel {
readonly version: FeedVersion.V1;
readonly title: string;
readonly home_page_url?: string;
readonly feed_url?: string;
readonly description?: string;
readonly user_comment?: string;
readonly next_url?: string;
readonly icon?: string;
readonly favicon?: string;
readonly author?: IFeedAuthor;
readonly expired?: boolean;
readonly hubs?: IFeedHub[];
readonly items: FeedItem[];
}
/**
* This interface is the all the feed item properties that are either
* mandatory or simply optional. In practice you will need a FeedItem
* instead as this has additional type constraints on content_html
* and content_text which have more complex requiredness
*/
export interface IBaseFeedItem {
readonly id: string;
readonly url?: string;
readonly external_url?: string;
readonly title?: string;
readonly summary?: string;
readonly image?: string;
readonly banner_image?: Date;
readonly date_published?: Date;
readonly date_modified?: Date;
readonly author?: IFeedAuthor;
readonly tags?: string[];
readonly attachments?: IFeedAttachment[];
}
export interface IBaseFeedItemContent {
readonly content_html: string;
readonly content_text: string;
}
/**
* FeedItems must have one of the content properties, this
* uses an intersection to describe all the allowed combinations.
*/
export type FeedItem = IBaseFeedItem & (
Pick<IBaseFeedItemContent, "content_html"> |
Pick<IBaseFeedItemContent, "content_text"> |
IBaseFeedItemContent
);
export interface IFeedAuthor {
readonly name?: string;
readonly url?: string;
readonly avatar?: string;
}
export interface IFeedHub {
readonly type: string;
readonly url: string;
}
export interface IFeedAttachment {
readonly url: string;
readonly mime_type: string;
readonly title?: string;
readonly size_in_bytes?: number;
readonly duration_in_seconds?: number;
}
export type Feed = IFeedTopLevel;
// https://stackoverflow.com/posts/48216010/revisions
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
/*
* These `Wire*` types represent the transformations done to a Feed to prepare it
* for serialization. This is an internal detail only
*/
type WireItems = Omit<FeedItem, "date_published" | "date_modified">
& { readonly date_published?: string, readonly date_modified?: string };
type WireFeed = Omit<Feed, "items">
& { readonly items: WireItems }
/**
* Converts a Date object into a string representing that date in UTC in ISO 3339 format
* https://www.ietf.org/rfc/rfc3339.txt
*
* While the ISO (8601) string format that Date supports is very close to 3339 lets not
* play fast and loose.
*
* This format string omits the time-secfrac portion of the time. That granularity isn’t
* needed (as yet).
*
* @param d the date to convert to string
*/
function dateToIso3339(d: Date): string {
const pad = (n: number) => n < 10 ? `0${n}` : n.toString();
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}Z`
}
function feedToWireFeed(feed: Feed): WireFeed {
const wire:any = _.clone(feed);
wire.items = feed.items.map(item => {
const newItem:any = _.clone(item);
if (item.date_modified) {
newItem.date_modified = dateToIso3339(item.date_modified);
}
if (item.date_published) {
newItem.date_published = dateToIso3339(item.date_published);
}
return newItem;
});
return wire;
}
function itemLastModified(item: FeedItem):Date | undefined {
if (item.date_modified) return item.date_modified;
if (item.date_published) return item.date_published;
return undefined;
}
/**
* This function will use the latest modified entry to determine the
* last modified date of the entire feed.
*
* @param feed The JSON feed to compute a lastModified date for
* @returns the last modified date if it can be determined, otherwise undefined
*/
export function feedLastModified(feed: Feed): Date | undefined {
const sortedItems = feed.items
.map(itemLastModified)
.filter(d => !!d)
.sort((a, b) => {
if (a! < b!) return -1;
if (a! > b!) return 1;
return 0;
}) as Date[];
if (sortedItems.length <= 0) return undefined;
return sortedItems[0];
}
/**
* Serialize a JSON Feed to an HTTP response
*
* This is a thin wrapper over the express json function that also computes
* a Last Modified date from feed items.
*
* @param res the express response to write this feed to.
*/
export function jsonFeedResponse(res: express.Response, feed: Feed) {
const lastModified = feedLastModified(feed);
if (lastModified) {
res.setHeader('Last-Modified', lastModified.toUTCString());
}
res.json(feedToWireFeed(feed));
}
Copyright (c) 2018, Andrew Brehaut
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment