tamj0rd2/ncdc

Expose a programatic API

tamj0rd2 opened this issue · 3 comments

It'd be cool if JS could be evaluated to create fixtures from the export of a javascript file.

I could even write a programmatic version of ncdc. You'd pass it some io-ts (or similar) functions rather than just a type name.

That has a couple benefits

  • not having to invoke the typescript compiler (ideally someone would just run ncdc using io-ts) which would speed everything up
  • defined type guards can be reused at runtime for whatever purposes people may have
  • whoever uses the programmatic version will probably be using typescript which means that people would be able to have dynamic responses

Mysteries:

  • How would dynamic fixtures get reloaded? (maybe I can check how knex are doing this for their ts migration files)
  • How would JSDOC be supported? Does io-ts support it?

Or even something like this. Lets say a user has a ncdc.ts file in their project. The below kinda mirrors what we have in the docs:

interface Book {
  ISBN: string
  ISBN_13: string
  author: string
  title: string
  inventoryId: string
}

const isBook = (responseBody: unknown): asserts responseBody is Book => {
  if (typeof responseBody !== 'object') {
    throw new Error('Expected val to be of type string')
  }
  // more validation logic would go here
}

const config = {
  services: {
    bookService: {
      port: 3000,
      realUrl: 'https://example.com',
      tsconfig: './path/to/tsconfig.json',
      rateLimit: 300,
      resources: [
        {
          name: 'Book not found',
          serveOnly: true,
          request: {
            method: 'GET',
            endpoints: '/api/books/a-bad-id',
          },
          response: {
            code: 404,
          },
        },
        {
          name: 'Book',
          request: {
            method: 'GET',
            endpoints: ['/api/books/123', '/api/books/456'],
            serveEndpoint: '/api/books/:id',
          },
          response: {
            code: 200,
            serveBody: {
              ISBN: '9780141187761',
              ISBN_13: '978-0141187761',
              author: 'George Orwell',
              title: ' 1984 Nineteen Eighty-Four',
              inventoryId: 'item-87623',
            },
            // validator would replace the old "type" property
            validator: isBook,
          },
        },
      ],
    },
  },
}

export default config

/**
 * the new usage of the CLI would be:
 * ncdc serve blah.ts --watch
 * ncdc test blah.ts
 *
 * maybe there'd also be an option to choose the particular services, e.g
 * ncdc serve blah.ts filmService --watch
 * ncdc test blah.ts bookService
 */

Things this would change

  • Because validation functions are passed as type, the typescript compiler wouldn't be needed anymore. Invoking the typescript compiler is what takes 99% of the time.
  • Those validation functions can be used elsewhere in peoples code - nice for runtime validation of http requests for example
  • Using ts means someone can now have dynamic responses - really nice if someone wants to provide a function for serveBody
  • not having to use Concurrently to run 4 mocks APIs at the same time (basically, this fixes #184) - this would be a pretty great performance gain too, specifically because we won't have 4 instances of the typescript compiler watching files
  • realUrl can now be provided via environment variables or any other way someone wants to
  • again, not sure how JSDOC would work after this - the answer is that it wouldn't. Users should provide their own validation functions that contain all of the validation logic required.
  • how do mocks get reloaded? Does someone need to restart ncdc every time they make a change? Maybe there would need to be a watchFiles glob array.

Things that stay the same

  • user would still interact with ncdc with the CLI for testing, but an additional programmatic serve API could be made available too
  • ncdc still needs access to (at least a subset of) the user's code
  • the user still needs to have any installed any dependencies that are required from within the ncdc.ts file
  • people can still import fixture files from disk if they want to

Taking a different, simpler approach:

Rather than using some serious magic, with an ncdc.ts, people can just write their own script that create an NCDC instance which exposes methods to create/test/generate etc.

Here's the same Books service example from the docs again, but also with a Films service that would be hosted on port 4001.

#!/usr/bin/env -S npx ts-node -P ./scripts/tsconfig.json ./scripts/fakes.ts

/* eslint-disable @typescript-eslint/no-var-requires */
import { resolve } from 'path'
import { NCDC, Method } from 'ncdc'

async function start(): Promise<void> {
  const ncdc = new NCDC(
    [
      {
        name: 'Books service',
        port: 4000,
        baseUrl: 'https://example.com',
        resources: [
          {
            name: 'Book not found',
            serveOnly: true,
            request: {
              method: Method.GET,
              endpoints: ['/api/books/a-bad-id'],
            },
            response: {
              code: 404,
            },
          },
          {
            name: 'Book',
            serveOnly: false,
            request: {
              method: Method.GET,
              body: undefined,
              endpoints: ['/api/books/123', '/api/books/456'],
              serveEndpoint: '/api/books/*',
            },
            response: {
              code: 200,
              headers: { 'content-type': 'application/json' },
              type: 'Book',
              serveBody: {
                ISBN: '9780141187761',
                ISBN_13: '978-0141187761',
                author: 'George Orwell',
                title: '1984 Nineteen Eighty-Four',
                inventoryId: 'bitem-87623',
              },
            },
          },
        ],
      },
      {
        name: 'Films service',
        port: 4001,
        baseUrl: 'https://example.com',
        resources: [
          {
            name: 'Serve error',
            serveOnly: false,
            request: {
              endpoints: [],
              serveEndpoint: '/api/*',
              method: Method.GET,
            },
            response: {
              code: 401,
              body: 'nice meme, lol',
              type: 'string',
            },
          },
        ],
      },
    ],
    { tsconfigPath: getPath('./tsconfig.json') },
  )

  if (process.argv.includes('--serve')) {
    await ncdc.serve({ watch: true })
  } else if (process.argv.includes('--test')) {
    await ncdc.test({})
  } else if (process.argv.includes('--generate')) {
    await ncdc.generate({ force: false, outputPath: getPath('json-schema') })
  } else {
    // a sensible-ish default if no flag is provided
    await ncdc.serve({ watch: true })
  }
  return
}

void start().catch((err) => {
  console.log('fatal error', err)
  process.exit(1)
})

function getPath(pathRelativeToRepoRoot: string) {
  return resolve(process.cwd(), pathRelativeToRepoRoot)
}

The string Book still refers to the name of the type.

Things this would change

  • Possibility in the future for dynamic responses - really nice if someone wants to provide a function for serveBody
  • not having to use Concurrently to run 4 mocks APIs at the same time (basically, this fixes #184) - this would be a pretty great performance gain too, specifically because we won't have 4 instances of the typescript compiler watching files
  • realUrl can now be provided via environment variables or any other way someone wants to
  • fixture files would not get reloaded. It would be up to the user to implement some watching logic for their files
    • ncdc serve command would probably need to expose a start method (it already has stop)

Things that stay the same

  • JSDOC would continue to work
  • user can still use the CLI until it's deprecated
  • ncdc still needs access to (at least a subset of) the user's code