functions/url.ts

Summary

Maintainability
D
2 days
Test Coverage
import { Env, CreateUrlFormData, UrlRow, DeleteUrlFormData, UrlDatatableResponse } from '../src/ts/types'
import { createUrl, deleteUrl, getRowByShortCode, getUrls } from '../src/ts/data'
import { DEFAULT_RESPONSE_HEADERS } from '../src/ts/const'
import { generateShortCode, isValidShortCode, isValidUrl } from '../src/ts/url'
import { convertFormDataToJson } from '../src/ts/form'

/**
 * Handle the GET (Read) method.
 *
 * @param {any} context The request context.
 * @returns {Promise<Response>} Response object.
 */
export const onRequestGet: PagesFunction<Env> = async function (context) {
  const output: UrlDatatableResponse = { data: [] }
  const allRows: UrlRow[] = await getUrls(context)
  for (const row of allRows) {
    output.data.push({ s: row.short, l: row.long, c: row.created })
  }

  return new Response(JSON.stringify(output), {
    status: 200,
    headers: DEFAULT_RESPONSE_HEADERS
  })
}

/**
 * Handle the POST (Create) method.
 *
 * @param {any} context The request context.
 * @returns {Promise<Response>} Response object.
 */
export const onRequestPost: PagesFunction<Env> = async function (context) {
  const headers = context.request.headers
  const input: CreateUrlFormData = convertFormDataToJson(await context.request.formData()) as CreateUrlFormData
  const requestDetails = {
    short: input.short,
    long: input.long,
    userAgent: headers.get('user-agent'),
    userCountry: headers.get('cf-ipcountry'),
    userLanguage: headers.get('accept-language'),
    userIp: headers.get('cf-connecting-ip'),
    cfRay: headers.get('cf-ray'),
    referer: headers.get('referer'),
    environment: headers.get('host')
  }

  // Make sure at least the long URL was supplied.
  if (requestDetails.long.length < 1) {
    return new Response(JSON.stringify({ action: 'create', status: 'error', message: 'Insufficient data supplied' }), {
      status: 400,
      headers: DEFAULT_RESPONSE_HEADERS
    })
  }

  // Validate long URL input
  if (!isValidUrl(requestDetails.long)) {
    return new Response(
      JSON.stringify({
        action: 'create',
        status: 'error',
        message: 'Invalid URL supplied. Correct the URL and try again.'
      }),
      { status: 400, headers: DEFAULT_RESPONSE_HEADERS }
    )
  }

  // Make sure the short code (if supplied) is valid and isn't already taken.
  if (requestDetails.short != null && requestDetails.short.length > 0) {
    if (!isValidShortCode(requestDetails.short)) {
      return new Response(
        JSON.stringify({
          action: 'create',
          status: 'error',
          message:
            'Invalid Short Code supplied. Short-codes must contain only alphanumeric and dash (-) characters. Correct the Short Code and try again.'
        }),
        { status: 400, headers: DEFAULT_RESPONSE_HEADERS }
      )
    }
    const shortRow = await getRowByShortCode(requestDetails.short, context)
    if (shortRow != null) {
      return new Response(
        JSON.stringify({
          action: 'create',
          status: 'error',
          message: 'Short URL already exists',
          short: requestDetails.short,
          long: shortRow.long
        }),
        { status: 400, headers: DEFAULT_RESPONSE_HEADERS }
      )
    }
  } else {
    // Generate a short code for the long URL
    let shortRow: UrlRow | null
    let count = 0
    do {
      const toShorten = requestDetails.long + (count > 0 ? count.toString() : '')
      const shortCodeEnvLength = context.env.SHORT_CODE_LENGTH
      requestDetails.short = await generateShortCode(
        toShorten,
        shortCodeEnvLength != null ? parseInt(shortCodeEnvLength) : undefined
      )
      // Make sure the auto-generated short code does not collide with an existing record.
      shortRow = await getRowByShortCode(requestDetails.short, context)
      count++
    } while (shortRow != null)
  }

  let output = ''
  let returnStatus = 200
  try {
    const createResult = await createUrl(requestDetails.short, requestDetails.long, context)
    if (createResult) {
      output = JSON.stringify({
        action: 'create',
        status: 'success',
        message: 'Short URL added',
        short: requestDetails.short,
        long: requestDetails.long
      })
    } else {
      output = JSON.stringify({
        action: 'create',
        status: 'error',
        message: 'Unable to add Short URL',
        short: requestDetails.short,
        long: requestDetails.long
      })
      returnStatus = 500
    }
  } catch (err) {
    output = JSON.stringify({
      action: 'create',
      status: 'error',
      message: 'Unable to add Short URL',
      short: requestDetails.short,
      long: requestDetails.long
    })
    returnStatus = 500
  }

  return new Response(output, {
    status: returnStatus,
    headers: DEFAULT_RESPONSE_HEADERS
  })
}

/**
 * Handle the DELETE method.
 *
 * @param {any} context The request context.
 * @returns {Promise<Response>} Response object.
 */
export const onRequestDelete: PagesFunction<Env> = async function (context) {
  const headers = context.request.headers
  const input: DeleteUrlFormData = convertFormDataToJson(await context.request.formData()) as DeleteUrlFormData
  const requestDetails = {
    short: input.short,
    userAgent: headers.get('user-agent'),
    userCountry: headers.get('cf-ipcountry'),
    userLanguage: headers.get('accept-language'),
    userIp: headers.get('cf-connecting-ip'),
    cfRay: headers.get('cf-ray'),
    referer: headers.get('referer'),
    environment: headers.get('host')
  }

  // Make sure the Short Code was supplied.
  if (requestDetails.short.length < 1 || !isValidShortCode(requestDetails.short)) {
    return new Response(JSON.stringify({ action: 'delete', status: 'error', message: 'Insufficient data supplied' }), {
      status: 400,
      headers: DEFAULT_RESPONSE_HEADERS
    })
  }

  const shortRow = await getRowByShortCode(requestDetails.short, context)
  if (shortRow === null) {
    return new Response(
      JSON.stringify({
        action: 'delete',
        status: 'error',
        message: 'Short URL not found'
      }),
      { status: 400, headers: DEFAULT_RESPONSE_HEADERS }
    )
  }

  let output = ''
  let returnStatus = 200
  try {
    const deleteResult = await deleteUrl(requestDetails.short, context)
    if (deleteResult) {
      output = JSON.stringify({
        action: 'delete',
        status: 'success',
        message: 'Short URL deleted',
        short: requestDetails.short
      })
    } else {
      output = JSON.stringify({
        action: 'delete',
        status: 'error',
        message: 'Unable to delete Short URL',
        short: requestDetails.short
      })
      returnStatus = 500
    }
  } catch (err) {
    output = JSON.stringify({
      action: 'delete',
      status: 'error',
      message: 'Unable to delete Short URL',
      short: requestDetails.short
    })
    returnStatus = 500
  }

  return new Response(output, {
    status: returnStatus,
    headers: DEFAULT_RESPONSE_HEADERS
  })
}