sounisi5011/metalsmith-netlify-published-date

View on GitHub
src/netlify.ts

Summary

Maintainability
D
2 days
Test Coverage
import './polyfills/symbol.async-iterator';

import parseLink from 'parse-link-header';

import { isObject } from './utils';
import { redirectFetch } from './utils/fetch';
import { debug } from './utils/log';

const log = debug.extend('netlify-api');
const requestLog = log.extend('request');
const responseLog = log.extend('response');
const responseHeadersLog = responseLog.extend('headers');
const responseErrorLog = responseLog.extend('error');

/**
 * @see https://github.com/netlify/open-api/blob/v0.11.4/swagger.yml#L1723-L1793
 */
export interface NetlifyDeployInterface {
    id: string;
    state: string;
    name: string;
    deploy_ssl_url: string;
    commit_ref: string | null;
    created_at: string;
    updated_at: string;
    published_at: string | null;
}

export type NetlifyDeployData = NetlifyDeployInterface & {
    deployAbsoluteURL: string;
};

export function isNetlifyDeploy(
    value: unknown,
): value is NetlifyDeployInterface {
    if (isObject(value)) {
        return (
            [
                'id',
                'state',
                'name',
                'deploy_ssl_url',
                'created_at',
                'updated_at',
            ].every(prop => typeof value[prop] === 'string') &&
            ['commit_ref', 'published_at'].every(
                prop => typeof value[prop] === 'string' || value[prop] === null,
            )
        );
    }
    return false;
}

export function addAbsoluteURL(
    deploy: NetlifyDeployInterface,
): NetlifyDeployData {
    return {
        ...deploy,
        deployAbsoluteURL: deploy.deploy_ssl_url.replace(
            /^(https?:\/\/)(?:(?!--)[^.])+(--)([^.]+)(\.netlify\.com)\/?$/,
            (match, scheme, hyphen, name, domain) =>
                name === deploy.name
                    ? scheme + deploy.id + hyphen + name + domain
                    : match,
        ),
    };
}

export const API_PREFIX = 'https://api.netlify.com/api/v1/';

export async function* netlifyDeploys(
    siteID: string,
    options: {
        accessToken?: string | null;
        commitHashList?: readonly string[];
        fetchCallback?: (
            url: string,
            headers: Partial<Record<string, string>>,
        ) => Promise<{ body: unknown; linkHeader?: string }>;
    } = {},
): AsyncIterableIterator<NetlifyDeployData> {
    const fetch =
        options.fetchCallback ||
        (async (url, headers) => {
            requestLog('GET %s / headers %o', url, headers);
            const result = await redirectFetch(url, { headers }).catch(
                error => {
                    responseErrorLog(
                        'fetch failed by http/https module error / %s / %o',
                        url,
                        error,
                    );
                    throw error;
                },
            );

            if (!result.isOk) {
                responseLog(
                    'fetch fails with HTTP %s %s / %s',
                    result.statusCode,
                    result.statusMessage,
                    url,
                );
                responseHeadersLog('headers of %s / %o', url, result.headers);
                throw new Error(
                    'Request to Netlify API failed. HTTP response status is: ' +
                        `${result.statusCode} ${result.statusMessage} ; ${url}`,
                );
            }
            responseLog('fetch is successful / %s', url);
            responseHeadersLog('headers of %s / %o', url, result.headers);

            const bodyText = await Promise.resolve(result.getBody())
                .then(String)
                .catch(error => {
                    responseErrorLog(
                        'failed to read response body / %s / %o',
                        url,
                        error,
                    );
                    if (error instanceof Error) {
                        error.message = `Request to Netlify API failed. Failed to read response body: ${url} ; ${error.message}`;
                    }
                    throw error;
                });

            let bodyData: unknown;
            try {
                bodyData = JSON.parse(bodyText);
            } catch (_) {
                responseErrorLog('invalid JSON body / %s', url);
                throw new SyntaxError(
                    `Netlify API returned invalid JSON from: ${url}`,
                );
            }

            const linkHeaderValue = result.headers.link;
            return {
                body: bodyData,
                linkHeader: Array.isArray(linkHeaderValue)
                    ? linkHeaderValue.join(', ')
                    : linkHeaderValue,
            };
        });
    const commitHashSet = options.commitHashList
        ? new Set(options.commitHashList)
        : null;
    const fetchedURL = new Set<string>();
    let lastURL: string | null = null;
    let initialDeploy: NetlifyDeployInterface | null = null;

    /**
     * @see https://www.netlify.com/docs/api/#deploys
     */
    let url = `${API_PREFIX}sites/${siteID}/deploys`;
    log('start fetching first page: %s', url);

    while (!fetchedURL.has(url)) {
        const lastURLSet = new Set<typeof lastURL>(lastURL);

        const headers = options.accessToken
            ? { authorization: `Bearer ${options.accessToken}` }
            : {};
        const { body, linkHeader } = await fetch(url, headers);
        fetchedURL.add(url);

        /**
         * @see https://www.netlify.com/docs/api/#pagination
         */
        let nextURL: string | null = null;
        if (linkHeader) {
            const linkData = parseLink(linkHeader);
            responseHeadersLog('pagination of %s / %o', url, linkData);
            if (linkData) {
                const nextLink = linkData.next;
                nextURL = (nextLink && nextLink.url) || null;

                const lastLink = linkData.last;
                if (lastLink && lastLink.url) {
                    lastURL = lastLink.url;
                }
            }
        } else {
            responseHeadersLog('"Link" header not found in headers / %s', url);
        }
        lastURLSet.add(lastURL);

        if (Array.isArray(body)) {
            const netlifyDeployList = body.filter(isNetlifyDeploy);

            const matchedDeployList = netlifyDeployList.filter(deploy => {
                if (deploy.state !== 'ready') {
                    return false;
                }

                if (!commitHashSet) {
                    return true;
                }

                const commitHash = deploy.commit_ref;
                if (commitHash !== null && commitHashSet.has(commitHash)) {
                    commitHashSet.delete(commitHash);
                    return true;
                }

                return false;
            });
            if (netlifyDeployList.length !== matchedDeployList.length) {
                responseLog(
                    'deploy list count: %d / among them, valid count: %d / %s',
                    netlifyDeployList.length,
                    matchedDeployList.length,
                    url,
                );
            } else {
                responseLog(
                    'deploy list count: %d / %s',
                    netlifyDeployList.length,
                    url,
                );
            }

            for (const deploy of matchedDeployList) {
                yield addAbsoluteURL(deploy);
            }

            const isLastDeployList = !nextURL || lastURLSet.has(url);
            if (isLastDeployList && netlifyDeployList.length >= 1) {
                const lastDeploy =
                    netlifyDeployList[netlifyDeployList.length - 1];
                if (lastDeploy.commit_ref === null) {
                    initialDeploy = lastDeploy;
                    if (!matchedDeployList.includes(initialDeploy)) {
                        responseLog(
                            'get the initial deploy from the response / %s',
                            url,
                        );
                        yield addAbsoluteURL(initialDeploy);
                    }
                }
            }
        } else {
            responseErrorLog(
                'response body is not an array: %o / %s',
                body,
                url,
            );
        }

        if (nextURL && (!commitHashSet || commitHashSet.size >= 1)) {
            log('start fetching next page: %s', nextURL);
            url = nextURL;
        } else if (lastURL && !initialDeploy) {
            log('start fetching last page: %s', lastURL);
            url = lastURL;
        }
    }
}