diff --git a/README.md b/README.md index a8a53d6..1ce8d1b 100644 --- a/README.md +++ b/README.md @@ -1 +1,546 @@ # moquerie + +> Effortlessly mock your entire API with simple configuration and a beautiful UI. + +Moquerie is a tool that allows you to easily create a fake GraphQL or REST API (or both at the same time). It is designed to be simple to use and easy to configure. + +## Main features + +- **Local Database** (automatically managed for you) + - **Deactivate rows** so they are not returned by the API without deleting them + - **Factories** to create table row (aka 'Resource Instances') (can be saved and committed to your repository) + - **Branches** (duplicate or empty) + - **Snapshots** (full or partial) (can be saved and committed to your repository) + - History +- Generate database tables (aka 'Resource Types') from GraphQL schema or TypeScript files +- Automatic **GraphQL server** +- **No-Code read queries** (for GraphQL) +- **Dashboard UI** +- Extensible with `.mock.ts` files or with plugins +- Typed APIs + +## Setup + +Install the `moquerie` package: + +```bash +pnpm install moquerie +``` + +Create a `moquerie.config.ts` (or `moquerie.config.js`) file in the root of your project: + +```ts +import { defineConfig } from 'moquerie/config' + +export default defineConfig({ + // API port + server: { + port: 4002, + }, + // GraphQL schema + graphql: { + schema: { + scanCodeFiles: './src/schema/**/*.ts', + }, + }, +}) +``` + +If you have a GraphQL schema, you can let moquerie scan your code files for graphql schema definitions that uses the `gql` tag. + +```ts +import { defineConfig } from 'moquerie/config' + +export default defineConfig({ + // API port + server: { + port: 4002, + }, + // GraphQL schema + graphql: { + schema: { + scanCodeFiles: './src/schema/**/*.ts', + }, + }, +}) +``` + +You also have several options to configure your GraphQL schema: + +- `url`: Live URL to the GraphQL server +- `jsonFile`: Introspection result JSON file +- `graphqlFiles`: `.graphql` files to load, can be a path to a single file or a glob pattern +- `scanCodeFiles`: Glob pattern to scan code files for GraphQL schema definitions that uses the `gql` tag + +For REST you don't need additional configuration, but your need to register API Routes. + +## Getting started + +Run the `moquerie` command to start the server: + +```bash +pnpm exec moquerie +``` + +Open your browser at the UI URL displayed in the console. + +![screenshot of home](./docs/home.png) + +You can see the mocked API endpoints listed here. + +### Resource types + +By default moquerie will infer resource types from your GraphQL schema. If you don't have one, you can define resource types for your REST API with the `rest.typeFiles` config: + +```ts +import { defineConfig } from 'moquerie/config' + +export default defineConfig({ + rest: { + typeFiles: [ + 'src/rest/types.ts', + ], + }, +}) +``` + +Here is an example that demonstrate several supported features such as importing types from other files, optional fields, union types, and deprecated fields: + +```ts +import type { MyObjectNotExported } from './other.js' + +export interface MyObject { + id: string + name: string + count: number +} + +/** + * Another object + * @restPath /foo + */ +export interface MyOtherObject { + id: string + /** + * Some useful description + */ + description?: string + otherDescription: string | undefined + thirdDescription: null | string + objects: MyObject[] + notExported: MyObjectNotExported + /** + * @deprecated Use `otherDescription` instead + */ + deprecatedField: string +} + +/** + * @deprecated Use `MyOtherObject` instead + */ +export interface OldObject { + id: string + name: string + count: number +} +``` + +You can also easily extend types for existing resource types (usually from a GraphQL schema) + +```ts +import { defineConfig } from 'moquerie/config' + +export default defineConfig({ + graphql: { + schema: { + scanCodeFiles: './src/**/*.ts', + }, + }, + + extendTypes: { + typeFiles: [ + 'src/extend/types.ts', + ], + }, +}) +``` + +Here is an example that demonstrate extending a type from a GraphQL schema: + +```ts +// Extend the Message type from the GraphQL schema +export interface Message { + /** + * Some example property added to the schema + */ + internalProp: string +} +``` + +### API Routes + +Every code you write for moquerie should be placed inside files ending with `.mock.ts`. Moquerie will automatically load these files for you. + +In addition to API routes, we can also define resolvers, scripts and much more as described later. + +Here is an example of a simple API route: + +```ts +// file-name.mock.ts + +import { defineApiRoutes, defineResolvers, defineSchemaTransforms, defineScripts } from 'moquerie/mocks' + +export default { + // Define API routes + ...defineApiRoutes((router) => { + router.get('/messages/count', () => 42) + }), +} +``` + +We recommend using the spread operator to merge the results of the `defineApiRoutes`, `defineResolvers`, `defineSchemaTransforms`, and `defineScripts` functions. For example: + +```ts +// file-name.mock.ts + +export default { + ...defineApiRoutes((router) => { + // Define API routes + }), + ...defineResolvers({ + // Define resolvers + }), + ...defineSchemaTransforms(({ schema }) => { + // Define schema transforms + }), + ...defineScripts({ + // Define scripts + }), +} +``` + +You can use the `router` object to define API routes. The `router` object has a method for each HTTP verb and each handler function receive a useful object as the parameter that allow you to access the database and other utilities. + +```ts +// file-name.mock.ts + +import { defineApiRoutes, defineResolvers, defineSchemaTransforms, defineScripts } from 'moquerie/mocks' + +export default { + // Define API routes + ...defineApiRoutes((router) => { + router.get('/messages/count', async ({ db }) => { + // There are many more methods available on the context object above + return (await db.Message.findMany()).length + }) + }), +} +``` + +## Database + +The Database page is a data explorer in which you can create, read, update, and delete rows (aka 'Resource Instances'). You can also deactivate rows so they are not returned by the API without deleting them. + +![screenshot of database](./docs/database.png) + +Instances that are active (open eye icon) will be taken into account when querying the API, while inactive instances (slashed eye icon) will be ignored - even if you call the database in resolvers! This is useful for testing different scenarios without having to delete and recreate instances constantly. + +You can select an instance to see its details, update it, or deactivate it. You can also select multiple instances to apply bulk changes using the `Shift` key. + +![screenshot of multiple selection](./docs/database-select-many.png) + +![screenshot of multiple selection](./docs/database-bulk-edit.png) + +### Branches + +To help you switch between different scenarios, you can create branches. Branches are like a copy of the database at a certain point in time. You can create a new branch from the current database, or you can create an empty branch. + +![screenshot of branches](./docs/database-branches.png) + +![screenshot of creating a branch](./docs/database-branch-create.png) + +## Factories + +Factories are simple functions that create a single row (aka 'Resource Instance') in the database. They can be saved and committed to your repository to be easily shared with your team. + +![screenshot of a factory](./docs/factory.png) + +Factories use [faker](https://github.com/faker-js/faker) to generate random data. + +![screenshot of faker ui in the factory](./docs/factory-faker.png) + +You can then use them to create instances in the database. + +![screenshot of a factory creating instances](./docs/factory-create-instances.png) + +Anytime you can save things to the current repository, you will see a toggle to switch between `Local` and `Repository`. + +![screenshot of the location toggle](./docs/factory-toggle-location.png) + +## Snapshots + +Snapshots are a way to save the state of the database at a certain point in time. You can save full snapshots or partial snapshots (only some resource instances). Similar to factories, snapshots can be saved and committed to your repository. + +![screenshot of a snapshot](./docs/snapshot.png) + +You can then edit, delete or import snapshots to your database. Note that data inside resource instances cannot be directly edited in the snapshot editor. + +## PubSub + +You can use the PubSub editor to publish real-time events to your API. This is useful for testing subscriptions or other real-time features. + +![screenshot of the pubsub editor](./docs/pubsub.png) + +## Scripts + +Scripts allows you to create complex scenarios that involve calling multiple factories, database operations and maybe more. + +Similar to API Routes, you need to define scripts in `.mock.ts` files. + +```ts +// file-name.mock.ts + +import { defineScripts } from 'moquerie/mocks' + +export default { + ...defineScripts({ + // Each key is a script + createSimpleMessage: { + description: `Create a simple message sent by current user`, + fn: async ({ generateResource, db }) => { + // Create message with a Factory + const [ref] = await generateResource('Message', 'SimpleMessageFactory') + + // Update message with current user + const me = await db.User.findFirstReference((data, { tags }) => tags.includes('me')) + if (!me) { + throw new Error(`User with tag 'me' not found`) + } + await db.Message.updateFirst({ + from: me, + }, (_, instance) => instance.id === ref.__id) + }, + }, + }), +} +``` + +You can run scripts from the UI or from the API. In the Dashboard UI, you will see a summary of the operations done by the script or any error that occurred. + +![screenshot of the script editor](./docs/script.png) + +## Resolvers + +Resolvers are functions that are called when a query is made to the API. They can be used to customize the response of the API, or to perform complex operations. Each resolver is a function accosiated with a specific field of a specific Resource Type in the schema. + +Similar to API Routes and Scripts, you need to define resolvers in `.mock.ts` files. + +```ts +import { defineResolvers } from 'moquerie/mocks' + +export default { + ...defineResolvers({ + // Each key is a Resource Type + // Target type is `Query` + Query: { + // Each key is a field of the `Query` type + + // field name is `manyHellosCount` + manyHellosCount: async ({ db }) => { + const query = await db.Query.findFirst() + return query?.manyHellos.length ?? 0 + }, + }, + + // Target type is `Mutation` + Mutation: { + // Each key is a field of the `Mutation` type + + // field name is `addHello` + addHello: async ({ input, db, pubsub }) => { + const query = await db.Query.findFirst() + const manyHellos = query?.manyHellos ?? [] + manyHellos.push(input.message) + await db.Query.updateFirst({ + manyHellos, + }) + pubsub.graphql.publish('helloAdded', { + helloAdded: input.message, + }) + return manyHellos + }, + + // field name is `removeHello` + removeHello: async ({ input, db, pubsub }) => { + const query = await db.Query.findFirst() + const manyHellos = query?.manyHellos ?? [] + const index = manyHellos.indexOf(input.message) + if (index !== -1) { + manyHellos.splice(index, 1) + await db.Query.updateFirst({ + manyHellos, + }) + } + pubsub.graphql.publish('helloRemoved', { + helloRemoved: input.message, + }) + return manyHellos + }, + + testMutation: () => true, + + addSimple: async ({ input, db, pubsub }) => { + const simple = await db.Simple.create({ + id: input.id, + }) + + // Publish resource instance value + + // Either pass the value directly if it comes from `db`: + // pubsub.graphql.publish('simpleAdded', { + // simpleAdded: simple, + // }) + + // Or pass the reference: + pubsub.graphql.publish('simpleAdded', { + simpleAdded: await db.Simple.findFirstReference(s => s.id === simple.id), + }) + + // This will not work as we lose the hidden flag: + // pubsub.graphql.publish('simpleAdded', { + // simpleAdded: { + // ...simple, + // }, + // }) + + return simple + }, + }, + + // Target type is `User` + User: { + // field name is `fullName` + fullName: ({ parent: user }) => `${user.firstName} ${user.lastName}` + } + }), +} +``` + +You can inspect which resolvers are detected by moquerie in the UI. + +![screenshot of the resolvers](./docs/resolvers.png) + +## History + +You can see the history of all the changes made to the database in the History page. + +![screenshot of the history](./docs/history.png) + +## REST Playground + +You can use the REST Playground to test your REST API. + +![screenshot of the rest playground](./docs/rest-playground.png) + +## GraphQL Playground + +You can use the GraphQL Playground to test your GraphQL API. + +![screenshot of the graphql playground](./docs/graphql-playground.png) + +## Schema Transforms + +You can make changes to the schema programmatically using schema transforms. This is useful for adding new internal fields for example. + +```ts +// file-name.mock.ts + +import { defineSchemaTransforms } from 'moquerie/mocks' + +export default { + ...defineSchemaTransforms(({ schema, createEnumField }) => { + schema.types.User.fields.customInternalField = createEnumField('customInternalField', [ + { value: 1, description: 'One' }, + { value: 2, description: 'Two' }, + { value: 3, description: 'Three' }, + ]) + }), +} +``` + +## Programmatic API + +You can also use moquerie programmatically. You always start by creating an instance of moquerie: + +```ts +import { createMoquerieInstance } from 'moquerie' + +const mq = await createMoquerieInstance({ + cwd: process.cwd(), + watching: true, + skipWrites: false, +}) +``` + +> [!TIP] +> You can set `watching` and `skipWrites` to `false` to disable watching and writing to the disk during tests. + +You can then pass the moquerie instance to most of the functions in the `moquerie` package. + +### Start server + +This will start the mocked APIs. + +```ts +import { startServer } from 'moquerie' + +await startServer(mq) +``` + +### Run a Script + +You can run a script by its id (the key used in `defineScripts`). + +```ts +import { runScript } from 'moquerie' + +const report = await runScript(mq, 'createSimpleMessage') +``` + +### Call a Factory + +```ts +import { createInstanceFromFactory, getFactoryByName } from 'moquerie' + +const factory = await getFactoryByName(mq, 'SimpleMessage') +const instance = await createInstanceFromFactory(mq, { + factory, + save: true, +}) +``` + +### Get the resolved context + +The resolved context of the moquerie instance is useful to access the database, the pubsub, the resource schema, the list of scripts, resolvers, and so on. + +```ts +const ctx = await mq.getResolvedContext() +``` + +### Call the Database + +You can call the database the same way you would in a resolver or a script using the resolved context. + +```ts +const ctx = await mq.getResolvedContext() +// You can even check for the tags +const me = await ctx.db.User.findFirstReference((data, { tags }) => tags.includes('me')) +``` + +### Cleanup + +You can stop and cleanup the moquerie instance and its server: + +```ts +await mq.destroy() +``` diff --git a/docs/database-branch-create.png b/docs/database-branch-create.png new file mode 100644 index 0000000..5db02ae Binary files /dev/null and b/docs/database-branch-create.png differ diff --git a/docs/database-branches.png b/docs/database-branches.png new file mode 100644 index 0000000..3895de1 Binary files /dev/null and b/docs/database-branches.png differ diff --git a/docs/database-bulk-edit.png b/docs/database-bulk-edit.png new file mode 100644 index 0000000..4fb0e14 Binary files /dev/null and b/docs/database-bulk-edit.png differ diff --git a/docs/database-select-many.png b/docs/database-select-many.png new file mode 100644 index 0000000..9265360 Binary files /dev/null and b/docs/database-select-many.png differ diff --git a/docs/database.png b/docs/database.png new file mode 100644 index 0000000..83086da Binary files /dev/null and b/docs/database.png differ diff --git a/docs/factory-create-instances.png b/docs/factory-create-instances.png new file mode 100644 index 0000000..1c03335 Binary files /dev/null and b/docs/factory-create-instances.png differ diff --git a/docs/factory-faker.png b/docs/factory-faker.png new file mode 100644 index 0000000..36a2084 Binary files /dev/null and b/docs/factory-faker.png differ diff --git a/docs/factory-toggle-location.png b/docs/factory-toggle-location.png new file mode 100644 index 0000000..622f74d Binary files /dev/null and b/docs/factory-toggle-location.png differ diff --git a/docs/factory.png b/docs/factory.png new file mode 100644 index 0000000..238d0c2 Binary files /dev/null and b/docs/factory.png differ diff --git a/docs/graphql-playground.png b/docs/graphql-playground.png new file mode 100644 index 0000000..25d27d1 Binary files /dev/null and b/docs/graphql-playground.png differ diff --git a/docs/history.png b/docs/history.png new file mode 100644 index 0000000..ba2c429 Binary files /dev/null and b/docs/history.png differ diff --git a/docs/home.png b/docs/home.png new file mode 100644 index 0000000..a322cc8 Binary files /dev/null and b/docs/home.png differ diff --git a/docs/pubsub.png b/docs/pubsub.png new file mode 100644 index 0000000..584093a Binary files /dev/null and b/docs/pubsub.png differ diff --git a/docs/resolvers.png b/docs/resolvers.png new file mode 100644 index 0000000..5dc4d20 Binary files /dev/null and b/docs/resolvers.png differ diff --git a/docs/rest-playground.png b/docs/rest-playground.png new file mode 100644 index 0000000..dee718c Binary files /dev/null and b/docs/rest-playground.png differ diff --git a/docs/script.png b/docs/script.png new file mode 100644 index 0000000..b161b45 Binary files /dev/null and b/docs/script.png differ diff --git a/docs/snapshot.png b/docs/snapshot.png new file mode 100644 index 0000000..6b12210 Binary files /dev/null and b/docs/snapshot.png differ diff --git a/packages/app/nuxt.config.ts b/packages/app/nuxt.config.ts index b2ff6a2..4e1cb75 100644 --- a/packages/app/nuxt.config.ts +++ b/packages/app/nuxt.config.ts @@ -1,7 +1,7 @@ // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ devtools: { - enabled: true, + enabled: false, }, ssr: false, diff --git a/packages/app/server/plugins/init.ts b/packages/app/server/plugins/init.ts index c38bd65..ab04e0e 100644 --- a/packages/app/server/plugins/init.ts +++ b/packages/app/server/plugins/init.ts @@ -1,4 +1,4 @@ -import { createMoquerieInstance } from '@moquerie/core' +import { createMoquerieInstance, startServer } from '@moquerie/core' import { getDefaultCwd } from '@moquerie/core/util' import { setMq } from '../utils/instance.js' @@ -8,5 +8,5 @@ export default defineNitroPlugin(async () => { }) setMq(mq) // Start server - await mq.getResolvedContext() + await startServer(mq) }) diff --git a/packages/moquerie/README.md b/packages/moquerie/README.md new file mode 100644 index 0000000..c0f3a5b --- /dev/null +++ b/packages/moquerie/README.md @@ -0,0 +1,5 @@ +# moquerie + +> Effortlessly mock your entire API with simple configuration and a beautiful UI. + +[Documentation](https://github.com/Akryum/moquerie) diff --git a/playground/mq-api.ts b/playground/mq-api.ts new file mode 100644 index 0000000..4cde4e7 --- /dev/null +++ b/playground/mq-api.ts @@ -0,0 +1,24 @@ +import { createInstanceFromFactory, createMoquerieInstance, getFactoryByName, getFactoryStorage, runScript, startServer } from 'moquerie' + +const mq = await createMoquerieInstance({ + cwd: process.cwd(), + watching: true, + skipWrites: false, +}) + +await startServer(mq) + +const report = await runScript(mq, 'createSimpleMessage') +console.log(report) + +const factory = await getFactoryByName(mq, 'SimpleMessage') +const instance = await createInstanceFromFactory(mq, { + factory, + save: true, +}) + +const ctx = await mq.getResolvedContext() +// You can even check for the tags +const me = await ctx.db.User.findFirstReference((data, { tags }) => tags.includes('me')) + +await mq.destroy() diff --git a/playground/src/schema.mock.ts b/playground/src/schema.mock.ts index 357ec1f..5601a07 100644 --- a/playground/src/schema.mock.ts +++ b/playground/src/schema.mock.ts @@ -98,7 +98,7 @@ export default { }), ...defineApiRoutes((router) => { - router.get('/hello', async ({ db }) => { + router.get('/messages/count', async ({ db }) => { return (await db.Message.findMany()).length }) }),