Giter Site home page Giter Site logo

pmcelhaney / counterfact Goto Github PK

View Code? Open in Web Editor NEW
95.0 7.0 13.0 6.41 MB

OpenAPI / Swagger to TypeScript generator and mock server

License: MIT License

JavaScript 36.62% TypeScript 57.58% Dockerfile 0.63% HTML 0.11% Handlebars 5.07%
front-end-development mock-server openapi openapi3 swagger typescript

counterfact's Introduction

MIT License TypeScript Coverage Status


Counterfact

A Mock Server for High-performing Front-end Teams

Quick Start | Documentation | Changelog | Contributing


Counterfact is a free and open source mock server designed to hit the sweet spot every front-end engineer craves: real enough to be useful but fake enough to be usable. It stands in for the back-end code that doesn't exist yet or is too complex / rigid to suit your front-end development and testing workflow.

Like your favorite pair of sweatpants, Counterfact is lightweight, flexible, and comfortable; it stretches and shrinks to fit your project's unique contours. Best of all, it makes your ass boss look good. Go ahead, try it on.

Why Use Counterfact?

  • ๐Ÿ๏ธ Effortless API Mocking: Say goodbye to back-end hassles. Counterfact allows you to build and test your front-end code independently of the rest of the stack.
  • ๐Ÿ‘Œ Flexibility at Your Fingertips: Effortlessly toggle between mock and real services, change behavior without losing state, simulate complex use cases and error conditions, etc.
  • ๐Ÿคฉ Instant Gratification: If you have Node installed and an OpenAPI document handy, you're one command away from a dependency-free workflow.
  • ๐Ÿ› ๏ธ Dev Tools on the server: That's what it feels like. Change code in a running server and see the effect immediately. It even has a REPL, like the JS console in your browser.
  • ๐Ÿ”„ High code, low effort: Wouldn't you love the simplicity of a "low code" / "no code" tool without giving up the flexibility and power you get from knowing how to write TypeScript? Inconceivable, you say? Don't knock it 'til you try it.
  • ๐Ÿ„ Fluid workflow: Optionally use existing OpenAPI/Swagger documentation to auto-generate TypeScript types. When your documentation changes, the types update automatically.
  • ๐Ÿ› Plays well with others: Counterfact works with anything that depends on a REST API, including web apps, mobile apps, desktop apps, and microservices. It requires zero changes to your front-end framework or code.

Yeah but...

  • ๐ŸŽญ There are already a bazillion tools for mocking APIs!
    And they all fall short in one way or another. Counterfact is a novel approach designed to address their shortcomings. Sometimes a low-fidelity prototype that returns mock data is sufficient. Sometimes we wish the mocks were stateful, i.e. after POSTing an item in the shopping cart we can GET that same item back out. Sometimes we want to test against the real server, but override the responses on one or two endpoints. Counterfact makes all of these use cases as simple as possible, but no simpler.
  • โ›ฎ I don't like code generators!
    Then you came to the right place! The code generator is optional. If you have an OpenAPI spec (which is highly recommended in any case), you can use it to automatically generate TypeScript types in a split second. As the spec changes, the types are automatically kept in sync. Most of the generated code is cordoned off in an area that you never need to change or even look at.
  • ๐Ÿฅต Maintaining both a mock server and a real server? That's just extra effort!
    People used to say the same thing about unit tests: it's twice as much code! Having spent nearly three decades writing front-end code, I've learned a lot development time is wasted messing with the back-end: getting the environment stood up, adding test data, logging in and out as different users, hitting the refresh button and waiting, etc. Counterfact eliminates that waste at a cost that is shockingly close to zero, and helps you maintain the flow state while developing.

10 Second Quick Start

To see Counterfact in action run the following command in your terminal:

npx counterfact@latest https://petstore3.swagger.io/api/v3/openapi.yaml api

This command installs Counterfact from npm, sets up a mock server implementing the Swagger Petstore, and opens a dashboard in a web browser. As long as you have Node 16 or later installed, it should "just work".

Documentation

For more detailed information, such as how to go beyond simple mocks that return random values, visit our tutorial and usage guide.

Similar Tools and Alternatives

While Counterfact offers a unique approach to API mocking that we believe provides the best overall DX, we understand the importance of having the right tool for your specific needs. Here are some similar tools and alternatives you might find useful:

Mirage JS has more or less the same goals as Counterfact and very different approaches to achieving them. Some notable differences are that it runs in a browser instead of Node, does not integrate with OpenAPI, and does support GraphQL.

If your goal is to get a server up and running quickly and your API doesn't do much beyond storing and retrieving data, JSON Server may be a great fit for you.

If your mocking needs are relatively simple and you're shopping for someone who has no reason to have Node installed their computer, Beeceptor, Mockoon, and Mocky.io are worth checking out. Mocky is free; the others have free and paid tiers.

Feedback and Contributions

We value all of your feedback and contributions, including ๐Ÿ’Œ love letters , ๐Ÿ’ก feature requests, ๐Ÿž bug reports, and โœ๏ธ grammatical nit-picks in the docs. Please create an issue, open a pull request, or reach out to [email protected].

Welcome to the bottom of the README club! Since you've come this far, go ahead and smash that like and subscโ€ฆ er, uh, give this project a โญ๏ธ on GitHub! ๐Ÿ™๐Ÿผ

counterfact's People

Contributors

0xghada avatar dethell avatar friederbluemle avatar github-actions[bot] avatar kameronkales avatar pmcelhaney avatar renovate-bot avatar renovate[bot] avatar tinydogio-joshua avatar yangsimin avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

counterfact's Issues

Lazily evaluate body

Somewhat speculative. In a case like this calculating both a text and a JSON body is wasteful, as we're only going to use one of them.

export async function GET() {
  return response[200].text(calculateText()).json(calculateJson()) ;
}

It's easy to address. We can optionally pass a function wherever a body is expected, and lazily evaluate that function.

export async function GET() {
  return response[200].text(() => calculateText()).json(() => calculateJson()) ;
}

Also, the body could be a ReadableStream or a generator.

These are edge cases. I haven't yet run into a need to support them. If and when we do, I want to make sure we have a plan and don't have to go back to the drawing board.

npx counterfact start

npx counterfact start should start up a server that looks for paths in the ./paths/ directory and a context object exported from ./context/context.js and start up a server.

For now:

  • if ./context/context.js does not exist or does not export an object named context it should provide {} as the context object
  • it only needs to run in TypeScript

In the future:

  • command line arguments for
    • where to find the paths
    • where to find the context
    • what port to run on
    • whether to open a browser pointing to Swagger
  • support for TypeScript

Here's some code to get started.

import { fileURLToPath } from "node:url";

import type { Middleware } from "koa";
import Koa from "koa";
import { counterfact } from "counterfact";
import { koaSwagger } from "koa2-swagger-ui";
import serve from "koa-static";
import bodyParser from "koa-bodyparser";
import open from "open";

import { context } from "./context/context.js";

const PORT = 3100;

const PATHS_DIRECTORY = fileURLToPath(new URL("paths/", import.meta.url));

const app = new Koa();

app.use(serve(fileURLToPath(new URL("public", import.meta.url))));

app.use(
  koaSwagger({
    routePrefix: "/docs",

    swaggerOptions: {
      url: "/openapi.yaml",
    },
  })
);

app.use(bodyParser());

// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const { koaMiddleware } = (await counterfact(PATHS_DIRECTORY, context)) as {
  koaMiddleware: Middleware;
};

app.use(koaMiddleware);

app.listen(PORT);
process.stdout.write("Counterfact is running.\n");
process.stdout.write(`See docs at http://localhost:${PORT}/docs\n`);
process.stdout.write(
  `A copy of the Open API spec is at ${fileURLToPath(
    new URL("public/openapi.yaml", import.meta.url)
  )}\n`
);
process.stdout.write(
  `The code that implements the API is under ${PATHS_DIRECTORY}\n`
);

if (process.argv.includes("--open")) {
  await open(`http://localhost:${PORT}/index.html`);
}

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Pending Status Checks

These updates await pending status checks. To force their creation now, click the checkbox below.

  • chore(deps): update dependency @swc/core to v1.7.11

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Detected dependencies

github-actions
.github/workflows/ci.yaml
  • actions/checkout v4
  • actions/setup-node v4
  • actions/cache v4
.github/workflows/codeql.yml
  • actions/checkout v4
  • github/codeql-action v3
  • github/codeql-action v3
  • github/codeql-action v3
.github/workflows/coveralls.yaml
  • actions/checkout v4
  • actions/setup-node v4
  • coverallsapp/github-action v2.3.0
.github/workflows/debug-windows.yaml
  • actions/checkout v4
  • actions/setup-node v4
  • actions/cache v4
.github/workflows/mutation-testing.yaml
  • actions/checkout v4
  • actions/setup-node v4
  • actions/cache v4
.github/workflows/release.yaml
  • actions/checkout v4
  • actions/setup-node v4
  • actions/cache v4
  • changesets/action v1
npm
package.json
  • @apidevtools/json-schema-ref-parser 11.7.0
  • @hapi/accept 6.0.3
  • @types/json-schema 7.0.15
  • ast-types 0.14.2
  • chokidar 3.6.0
  • commander 12.1.0
  • debug 4.3.6
  • fetch 1.1.0
  • fs-extra 11.2.0
  • handlebars 4.7.8
  • http-terminator 3.2.0
  • js-yaml 4.1.0
  • json-schema-faker 0.5.6
  • jsonwebtoken 9.0.2
  • koa 2.15.3
  • koa-bodyparser 4.4.1
  • koa-proxies 0.12.4
  • koa2-swagger-ui 5.10.0
  • lodash 4.17.21
  • node-fetch 3.3.2
  • open 10.1.0
  • patch-package 8.0.0
  • precinct 12.1.2
  • prettier 3.3.3
  • recast 0.23.9
  • typescript 5.5.4
  • @changesets/cli 2.27.7
  • @stryker-mutator/core 8.5.0
  • @stryker-mutator/jest-runner 8.5.0
  • @stryker-mutator/typescript-checker 8.5.0
  • @swc/core 1.7.10
  • @swc/jest 0.2.36
  • @testing-library/dom 10.4.0
  • @types/jest 29.5.12
  • @types/js-yaml 4.0.9
  • @types/koa 2.15.0
  • @types/koa-bodyparser 4.3.12
  • @types/koa-proxy 1.0.7
  • @types/koa-static 4.0.4
  • @types/lodash 4.17.7
  • copyfiles 2.4.1
  • eslint 8.57.0
  • eslint-config-hardcore 47.0.1
  • eslint-formatter-github-annotations 0.1.0
  • eslint-import-resolver-typescript 3.6.1
  • eslint-plugin-etc 2.0.3
  • eslint-plugin-file-progress 1.5.0
  • eslint-plugin-import 2.29.1
  • eslint-plugin-jest 28.8.0
  • eslint-plugin-jest-dom 5.4.0
  • eslint-plugin-no-explicit-type-exports 0.12.1
  • eslint-plugin-prettier 5.2.1
  • eslint-plugin-unused-imports 4.1.3
  • husky 9.1.4
  • jest 29.7.0
  • node-mocks-http 1.15.1
  • nodemon 3.1.4
  • rimraf 6.0.1
  • stryker-cli 1.0.2
  • supertest 7.0.0
  • using-temporary-files 2.2.1
  • node >=17.0.0

  • Check this box to trigger a request for Renovate to run again on this repository

Response builder fluent API

Add a parameter to the verb functions called response. The API looks like this.

response[statusCode]
  .header("a-string-header", "value")
  .header("a-number-header", 100)
  .match("content/type", "body")
  .match("another/type", "body")
  .json({ isJson: true }) // shortcut for .match("application/json", ...)
  .text("text") // shortcut for .match("text/plain", ...)
  .html("<p>html</p>"); // shortcut for .match("text/html", ...)

The return value is a response object.

const response = {
   statusCode: statusCode,
   headers: { ... },
   contentType: {
        "text/plain": "text body",
         "application/json": {json: "body"}
    }
}

If someone else picks this up I'll add more detail. But I'll probably get to it myself today.

Change the structure of generated code to use exceptions

Will add more later, typing on my phone.

In order to make the generated code look more like real code maybe 4xx and 5xx status codes should come from exceptions. Or maybe guard clauses.

Something like this:

if(rollTheDice()) { return { status: 500 } };

response.random()

Building on #133

response[statusCode].random() returns a valid, randomly generated response, based on the schema provided by an OpenAPI document.

For now, response[statusCode].header('x-header', 'whatever').json('whatever').random() ignores the header and content that were provided and provides a completely random value. In the future we'll want to keep whatever was provided and only randomize the parts that were not specified. In the future we'll want to keep whatever is provided and only randomize the remainder.

In order for random() to work we need to provide Counterfact the OpenAPI document at runtime. It can be passed into the constructor. If Counterfact doesn't have an OpenAPI spec, return a 500 error with an explanation.

This issue does not include type checking. That's covered in #136

Simple API for simple response

After #125 it will be possible to do this:

export function GET () {
  return "hello world"
}

We can distinguish a return type that is just the body from a return type that's the status, headers, and body by tacking a symbol onto the latter response. #125 makes it easy to tack on that symbol transparently.

Interact with the context via the the browser's dev tools (remote procedure call)

Similar to #86

For testing purpose, I would like to make access runtime access to the context object at the heart of a server as convenient as possible. Why not expose it through the browser's dev tools?

Not necessarily direct access. What I'm thinking is we create a proxy context object that runs in the browser, with method signatures matching the context object on the server. When you call context.reticulate('splines') on the browser object, it sends the method name and arguments to the server via a web socket. Counterfact then intercepts that request on the server side and calls context.reticulate('splines') on the server object. The return value is then JSON.stringify()ed and returned to the client.

Two consequences of this remote procedure call (RPC) approach:

  1. It only works on methods that take JSON-serializable objects as arguments and return JSON-serializable values. Maybe we can use TypeScript to enforce that all context methods follow this convention. That feels like a good idea anyway, as it will encourage people to move business logic into the Context class instead of the /paths/**/*.ts files.

  2. Every method on the client-side context object is asynchronous. If we care about the return value we need to use await x = context.reticulate('splines') even if the reticulate() method is synchronous on the server side.

> 90% unit test coverage

Depends on #24

We may need to exclude a few hard-to-reach lines. Other than that 100% coverage is certainly achievable.

Record, edit, and replay workflows

Record

There are two places where we can hook in and record how a client is interacting with the server:

  1. HTTP requests
  2. calls to methods on the context object

The last 100 or so of each of these actions will be recorded in memory. When the server crashes or we request it to do so via counterfact.record(nameOfRecording), Counterfact will write out two log files that are actually executable TypeScript files. Each line of the file is a call to a function. A log file will look something like this:

// ./logs/context/2022_07_21_1946.ts
export default async function log_2022_07_21_1946(context) {
await context.addUser({name: 'Alice', age: 25}); // -> 1
await context.addUser({name: 'Bob', age: 55}); // -> 2
await context.usersCount(); // -> 2
// begin make Alice an admin
await context.getUserById(1); // -> {id: 1, name: 'Alice', age: 25, isAdmin: false}
await context.makeAdmin({id: 1, name: 'Alice', age: 25, isAdmin: false}); // -> true
}
// ./logs/http/2022_07_21_1946.ts
export default async function log_2022_07_21_1946(client) {
await client.post('/user', {name: 'Alice', age: 25}) // -> 1
await client.post('/user', {name: 'Bob', age: 55}) // -> 2
await client.post('/user-count') // -> 2
// begin make Alice an admin
await client.get('/user/1'); // -> {id: 1, name: 'Alice', age: 25, isAdmin: false} 
await client.post('/user/1/make-admin'); -> true
await client.get('/user/1'); // -> {id: 1, name: 'Alice', age: 25, isAdmin: true} 
// now Alice should be an admin
}

The comments would have been inserted by the operator with calls to the context object, probably via the browser's dev tools.

counterfact.comment('begin make Alice an admin');
counterfact.comment('now Alice should be an admin');

Which might have been triggered a call to Counterfact's own rest API

POST /counterfact/comment

Which in turn might have been called from a GUI.

But let's not get ahead of ourselves. Plumbing first, porcelain follows.

Edit

Once the logs have been recorded, we can grab snippets, massage the code if necessary, and commit it in workflow files.

// ./workflows/story-12345/make-alice-an-admin.ts
export default function (counterfact) {
    const context = counterfact.context("/users");
    const user = context.getUserByName('Alice');
    await context.makeAdmin(user);
}

Replay

Now we can replay that workflow by calling:

counterfact.runWorkflow('make-alice-an-admin');

Again, that's just the plumbing. In the long run, we may build additional APIs and or UIs that make most of this work a matter of clicking and dragging widgets on a web app.

Define TypeScript type for the response builder [almost done]

We need to define the type for #133

Something like this:

type Response = {
   [status: StatusCode]: {
       headers: Record<string, number | string>,
       content: {
          media: MediaType,
          body: unknown
       }
   },
   match: (MediaType, unknown) => Response,
   json: (MediaType, unknown) => Response,   
   text: (MediaType, unknown) => Response,
   html: (MediaType, unknown) => Response,
   random: () => Response
}

When we generate the code, we can build a type that is much more specific (e.g. filling in the unknowns). This generic type is enough to get started.

Overwrite individual functions in paths/**/*.ts files if they haven't changed

Let's say Counterfact generates a GET function and a POST function in /paths/pets.ts

We change the GET function but leave the POST function alone. After making a change to the OpenAPI document, we want to recreate all generated code without overwriting manually edited code.

For that reason, Counterfact currently will not touch any file under paths if a file already exists.

Instead of ignoring the file completely, ideally it would leave the GET function alone and recreate the POST function.

To make this work, we can create an index file which keeps track of all generated functions and hashes of their ASTs. If a given function already exists, but it's hash has not changed, we know it's safe to recreate that function. Otherwise leave it alone.

Theoretically this same logic could be applied to other files, allowing users to make changes to the generated types if they so desire. I don't think we should encourage that behavior but it could be an option that's off by default.

Much better API for describing responses

I'm tired of looking at repetitive code like this.

export const GET: HTTP_GET = ({ path, context, tools }) => {
    const statusCode = tools.oneOf(["200", "400", "404"]);
  
    if (statusCode === "200") {
      if (tools.accepts("application/xml")) {
        return {
          status: 200,
          contentType: "application/xml",
          body: tools.randomFromSchema(PetSchema) as Pet,
        };
      }
      if (tools.accepts("application/json")) {
        return {
          status: 200,
          contentType: "application/json",
          body: tools.randomFromSchema(PetSchema) as Pet,
        };
      }
    }
    if (statusCode === "400") {
      return {
        status: 400,
      };
    }
    if (statusCode === "404") {
      return {
        status: 404,
      };
    }
  
    return {
      status: 415,
      contentType: "text/plain",
      body: "HTTP 415: Unsupported Media Type",
    };
  };

Instead of generating all this code, we could have a function that takes the entire response object and calculates a random valid response at runtime.

  export const GET: HTTP_GET = ({ path, context, tools }) => {
    return tools.randomResponse(GetSchema) as GetResponse;
  };

Originally I thought all that generated code was helpful because it's sort of self documenting. It's not really. What would be more helpful is if there was a JSDoc comment that outlines the possible responses along with descriptions. Also it might be convenient to have a status object that lists the possible responses via intellisense.

intellisense showing options 400_BAD_REQUEST and 404_NOT_FOUND

Ooooh, each status code could map to a function that takes two typed arguments: a response body and a headers object.

  export const GET: HTTP_GET = ({ path, context, tools }) => {
    const pet = context.getByById(path.id);
    if (!pet) return status["404_NOT_FOUND"]() 
    return  status["200_OK"](pet, {"x-some-header": "header"});
  };

That would be really ergonomic. It accomplishes my original goal of "self-documenting" but it's all through intellisense. If you want to know what kind of responses the operation can return, type status.. Select a response and you'll get a function that tells you what type you need for the response body and what (if any) headers you should supply.

Oh yeah. Playing around with this in VSCode and it is so nice! Adding to the 1.0 milestone. This is a killer feature.

Multiple content types in a response object

Handle multiple response types in the same return statement (without if (accepts(...)))).

Currently the return type of a verb function looks like this:

type returnType = {
   status: HttpStatusCode,
   contentType: `${string}/${string}",
   headers: Record<string, string | number>,
   body: unknown
}

We need a way to handle a scenario where the API can return two or more different content types depending on what the client accepts.

type returnType = {
   status: HttpStatusCode,
   headers: Record<string, string | number>,
  content: { 
          media: `${string}/${string}",
          body: unknown
      }[],
}

In this case, the server will loop through each item in the content array and return the content-type + body of the first one that matches. If there are no matches it will return a 415 status code (or whatever code is most appropriate, need to double check that).

Embeddable API demo widget

I'm looking for a web component that renders a widget with a dropdown for request method, a text box for URL, and a button to fetch the URL. Clicking the button invokes a request and renders the response below.

Code:

<api-widget methods="GET,POST" url="/hello/world">

Output:

[GET v] [/hello/world        ] [Go]
[POST v] [/hello/world       ] [Go]
Request body:
----------------------------------
|                                |
|                                |
|                                |
|                                |
|                                |
----------------------------------

After clicking the "Go" button:

[GET v] [/hello/world       ] [Go]
Response:
----------------------------------
| HTTP/2 200 Ok                  |
| Connection: Closed             |
| Content-Type: application/json |
|                                |                               
|                                |
| {                              |
|   message: "hello world"       |
| }                              |
----------------------------------

It doesn't have to be an exact replica of what I described. Anything that roughly fits the description and is implemented as a web component will do.

I want it to use in the documentation. It has nothing to do with Counterfact per se. If there's not already something out there, I'm hoping someone will go build one and release it as a separate npm package that Counterfact can import.

Context factory

Instead of a single context object, it might be useful to have a factory that produces a potentially different context object for each path / request method. This gets a little bit tricky with TypeScript but I don't think it's impossible. Will add more detail later.

class Context {
  setFoo() {

  }

  getFoo() {

  }

}

type WithPrefixes<Type, Prefixes extends string> = {
    [Property in keyof Type]: Property extends `${Prefixes}${string}` ? Type[Property] : undefined
};

const context:Context = {
  getFoo() {},
  setFoo() {}
}


type ReadOnlyPrefixes = "get" | "has" | "is" | "find"

type RequestMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";


function createContext<Path extends string, M extends RequestMethod> (path: string, method: M)   {
  return context as  M extends "GET" ? WithPrefixes<Context, ReadOnlyPrefixes> : Context
}


const contextForGet = createContext("/", "GET")
contextForGet.getFoo(); 
contextForGet.setFoo(); // should not be allowed 

const contextForPost = createContext("/", "POST");
contextForPost.getFoo(); 
contextForPost.setFoo(); // ok 


type TypeForPetsSlashTypesDotTS = ReturnType<typeof createContext<"/pets", "GET">>

measure test coverage

Ideally use Stryker to provide mutation testing on top of simple test coverage. Since the tests depend on experimental VM module support, we may not be able to get Stryker to work everywhere, so we may need to rely on Istanbul for some tests. The build should fail if there's less than n% coverage (ratcheting up quickly).

Point Counterfact to a JS file rather than a directory

Currently the base path is expected to be a directory.

my-app
   /routes <-- here
      /articles
         /[articleId].js
      /categories.js

However, the base path could be a file.

my-app
   /routes
      /articles
         /[articleId].js
      /categories.js
   /routes.js <-- here

And that makes a lot of sense, because that top level file could implement default handlers for each of the request methods.

User-defined context (instead of "store")

The store argument will be renamed "context" and optionally supplied by a user-defined class. The location of the path will be the second argument of the counterfact function.

const { koaMiddleware } = await counterfact(
  fileURLToPath(new URL("routes/", import.meta.url))
  fileURLToPath(new URL("context.ts", import.meta.url))
);

The context file (which could be either JS or TS) will export a class whose constructor has no arguments. Counterfact will instantiate the class into a singleton and pass that singleton around as the context (nee "store") argument in route handlers. If a second argument is not passed it will set the context to an empty object ({}) as it does now.

A user-defined context file will allow the us to define properties and methods in one place, as opposed to the currently anything-goes store object. It will also allow us to initialize state.

Once typescript support is added, we can also use it to generate interfaces from OpenAPI and make handler methods type safe.

// ./routes/user/[id]/hello.ts
import { Methods_hello as Methods } from "_types.ts"; // <-- this file will be automatically generated

export const GET: Methods.GET = ({context, path}) => {
    return {
       status: 200,
       body:  "Hello, context.userById(path.id).name!"
    }
}

Finish package.json

  • add metadata like "keywords" and "bugs"
  • add exports
  • install and make sure it actually works (when installed as a dependency)

load an openapi spec from the web

Currently the generate command expects the OpenAPI spec to be a file path. It should also work if passed an HTTP or HTTPS URL.

In other words, loadFile() in specification.js should notice when the path is a URL and load the URL.

parameterized paths

Sometimes a parameter appears in the middle of a path, e.g.:

/users/{user_id}/profile
/users/{user_id}/friends
/users/{user_id}/preferences

The filesystem based routing scheme doesn't currently accommodate this use case well. Ideally we would model the above like so:

- users
   - user_id
       - friends.js
       - preferences.js
       - profile.js

But that will only match these URLs

/users/user_id/friends
/users/user_id/preferences
/users/user_id/profile

Whereas we want it to match URLs that look like this:

/users/12345/friends
/users/12345/preferences
/users/12345/profile

/users/86753/friends
/users/86753/preferences
/users/86753/profile

/users/99999/friends
/users/99999/preferences
/users/99999/profile

We need a way to identify that the user_id directory is a wildcard, not meant to be matched literally. Where would be the right place to encode that information? How about in user.js?

- users
   - user_id
       - friends.js
       - preferences.js
       - profile.js
- users.js <--- here
// users.js 
const USER_ID_REGEX = /^\d{3,10}$/;

export function wildcard(key) {
   if(USER_ID_REGEX.test(key) { 
       return 'user_id';
   } 
   return key;
}

When parsing the URL /users/99999/profile, Counterfact will look at users.js and find that it has a wildcard function. Since "99999" is a valid user ID, it will next look for ./users/user_id.js and ./users/user_id/profile.js instead of ./users/99999.js and ./users/99999/profile.js

Profile.js will then look something like this:

//profile.js

export function GET({path, store}) {
   return {
      body: `User profile for ${path.user_id}: ${store.users[path.user_id].profile}`
   }
}

export function PUT({path, store, body}) {
    store.users[path.user_id].profile = body;
   return {
      body: `ok`
   }
}

Generate TypeScript files from OpenAPI

Once TypeScript support is available we can start auto-generating a lot of the code from OpenAPI.

  • Automatically stub out all of the files under routes.
  • For each path, create a*.types.ts file that exports type signatures for request handler functions (GET, POST, etc.)
  • The *.types.ts files are referenced by the path files
  • The *.types.ts files will reference another file or files that defines the schemas.
// ./routes/user/[id]/hello.ts
import type { HTTP_GET  } from "./hello.types";

export const GET: HTTP_GET = ({query}) => {
  return {
    body: `Hello, ${query.name ?? "stranger"}`
  }
}
export type HTTP_GET_REQUEST = { query: { name?: string, store: any } }

export type HTTP_GET = (request: HTTP_GET_REQUEST) => { body: string } | { status: 201 } | { body: string[], status: 404 }

If a route file already exists, it will not overwrite. However *.types.ts files will be overwritten. We have no reason to edit those files manually.

SchemaTypeCoder should handle "additionalProperties"

Given the following schema:

type: object
  additionalProperties:
    type: integer

SchemaTypeCoder should yield the following type:

{
    [k: string]: number
}

However, given the following schema.

type: object
  properties:
    name: firstName
    type: string 
  additionalProperties:
    type: integer

All properties except name should be numbers. TypeScript currently has no way to represent an object with some properties that have one type and all other properties of another type. The best we can do is this:

{
    name?: string
    [k: string]: unknown
}

It also needs to handle the case where additionalProperties is true or false.

Now that I've written it out I realize it boils down to something like this.

if (hasAdditionalProperties) {
    if(hasProperties) {
       append('[k: string]: unknown')
    } else {
       append(`[k: string]: ${additionalProperties.type}`)
    } 
}

I don't really care about this case at this point, but if ALL of the properties happen to have the same type as additionalProperties, then we can resolve the type instead of putting unknown in.

Build ASTs instead of printing code directly

The TypeScript generator might be easier to read, maintain, and test if it built up ASTs rather cobbling strings together with template literals.

A couple of libraries we could use:

It can be done one Coder at a time and we may decided part of the way through that it's not a good idea. The first step is to implement the base Coder#write() method like so:

write(script) {
    return printer.print(this.buildAst(script)); 
}

Then within each subclass create a buildAst() method and delete the write() method.

The CLI code is meh

I don't have a ton of experience building CLIs in Node and haven't put a lot of effort into this one.

The code works but it's messy. I'd like to use something like Commander

Or maybe play with the new parseargs library in Node 18.3

Change generated code to use `response.random()`

Depends on #135

Once that feature is available, the generated functions under paths can be reduced to something like this.

export function GET({context, response}) { /* plus query, path, body and/or headers if any of them have parameters */ 
   return response.random(); 
}

TypeScript support

Counterfact should support TypeScript as well as JavaScript (in the files defining routes).

The Counterfact source code itself doesn't need to be converted back to TypeScript to make this happen.

Generate JSDocs

OpenAPI has a description field. If we surface descriptions in the code and IDE we're more likely to write clear descriptions. A virtuous cycle.

HTTP response status code

When a handler returns a status code, e.g.:

return {
    status: 404,
    body: "not found"
}

That status code should be used in the HTTP response.

init command to create create a complete package

npx counterfact init ./path/to/openapi.yaml ./path/to/package

Counterfact should create a package that looks similar to the current demo-ts directory. So that in the end these four commands will result in a running server.

npx counterfact init ./path/to/openapi.yaml ./path/to/package
cd ./path/to/package
npm install
npm start

In time, we'll add prompts to make sure the user really wants to create a whole new package, whether they want it to use Koa or Express, whether it should run npm install, initialize a git repo, commit the code, generate unit tests, etc. For version 1.0, all that matters is that we make getting started and kicking the tires redonkulously easy.

Make the generated code pass our ESLint rules

I had to ignore the demo-ts directory in the eslintrc.json because a lot of the generated code does not satisfy the project's ESLint rules.

The generated code should be up to the same standards as the codebase itself. I'm anticipating 5-10 small PRs to get there.

REPL

While Counterfact is running, it would be great if we could interact with the context object in a REPL. It seems like a low hanging fruit but I haven't had much luck with REPL + TypeScript + ESM.

Create a CONTRIBUTING.md page

Basic instructions on how to fork the repo, install, run tests, etc. It doesn't have to be very detailed for the first cut. Move the information on using dev containers to this page and link to it from README.md.

Explain what separates this project from prior art

Counterfact was originally intended to solve some of the same problems as the items listed on this page:

https://mswjs.io/docs/comparison

Now it's also taken on generating TypeScript code from OpenAPI. Because I couldn't find anything that generates TypeScript code that I found to be sufficiently readable and easy to relate back to the source.

https://openapi-generator.tech/docs/generators/

And I have some other big ideas that I haven't written down yet, like building automated acceptance tests, fiddling with the server's state via a REPL, and building some kind of IDE plugin to trace a type back to its source in the Open API spec.

hot reload doesn't work in some cases

  1. run yarn start demo
  2. open http://localhost:3100/hello/world
  3. make a change to ./demo/hello/{name}.js
  4. reload http://localhost:3100/hello/world

The change made in step 3 should be reflected.

It's not because of this line in module-loader.js

        const url = `/${nodePath.join(parts.dir, parts.name)}`;

The path already starts with a / so the URL ends up starting with //.

I think it depends on whether parts.dir ends with a /.

Anyway, I think it's an easy fix. Just wrap the whole thing in nodePath.normalize().

always import node:path as nodePath

The word "path" in this codebase can mean a few different things:

  • a file path
  • a path in the tree structure of a JSON / YAML file
  • node's built-in path module

To ease the confusion, everywhere we import path, the code should be:

import nodePath from "node:path";

Avoid these patterns:

import path from "node:path";
import { join } from "node:path"; 

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.