Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: CI

on:
push:
branches:
- 'feat/**'
- 'develop'
pull_request:
branches:
- main
- develop

permissions:
contents: read

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5

- uses: actions/setup-node@v5
with:
node-version: "22.14"
cache: 'npm'

- run: npm ci
- run: npm run build
- run: npm run test:coverage

- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
38 changes: 16 additions & 22 deletions .github/workflows/publish-npm.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
# Publishes with a long-lived npm token from the GitHub Environment "NPM_TOKEN" (secret name: NPM_TOKEN).
# Use a classic Automation token or a granular token with publish access + "Bypass 2FA" — avoids EOTP.
# Optional: later you can switch to OIDC trusted publishing (see https://docs.npmjs.com/trusted-publishers).
name: Publish to npm

on:
Expand All @@ -13,41 +10,38 @@ concurrency:
group: npm-publish-${{ github.workflow }}
cancel-in-progress: true

permissions:
contents: write
issues: write
pull-requests: write
id-token: write

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

permissions:
contents: read

jobs:
publish:
release:
runs-on: ubuntu-latest
environment: NPM_TOKEN

steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
persist-credentials: false

- uses: actions/setup-node@v5
with:
node-version: "22.14"
registry-url: https://registry.npmjs.org
cache: 'npm'

- run: npm ci

- run: npm run build
- run: npm test

- name: Verify npm authentication
run: npm whoami
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Publish to npm
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
VERSION=$(node -p "require('./package.json').version")
if npm view "@xirconsss/zero-mock@${VERSION}" version >/dev/null 2>&1; then
echo "::notice::@xirconsss/zero-mock@${VERSION} is already on npm — skipping publish (avoid duplicate E403)."
exit 0
fi
npm publish
run: npx semantic-release
17 changes: 10 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Dependencies
node_modules/

# Build
# Build output
dist/

# Logs
Expand All @@ -14,19 +14,22 @@ pnpm-debug.log*
# Environment
.env
.env.*
.env.local

# OS
.DS_Store
Thumbs.db

# IDE
# IDE / Editor
.idea/
.vscode/

# Local test / scratch data
test-db.json
*.local.json
*.swp
*.swo

# Cursor / Claude (local-only)
.cursorrules
.claude/
.claude/

# Temp files
*.tmp
.zero-mock-*.tmp
1 change: 1 addition & 0 deletions .husky/commit-msg
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npx --no -- commitlint --edit "$1"
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npm test
28 changes: 28 additions & 0 deletions .releaserc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
[
"@semantic-release/changelog",
{
"changelogFile": "CHANGELOG.md"
}
],
[
"@semantic-release/npm",
{
"npmPublish": true,
"provenance": true
}
],
"@semantic-release/github",
[
"@semantic-release/git",
{
"assets": ["package.json", "package-lock.json", "CHANGELOG.md"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
]
]
}
41 changes: 41 additions & 0 deletions GEMINI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Zero-Mock: Project Overview

`zero-mock` is a zero-config CLI tool that generates a fully functional, persistent REST API from a JSON file.

## 🏗 Core Architecture

The project uses a modular singleton-based architecture with in-memory data strictly synchronized to a backing JSON file.

### Core Components

- **CLI Entry (`src/index.ts`)**: Harnesses `commander` to handle CLI arguments (`--file`, `--port`, `--delay`, `--watch`) and orchestrates the startup process.
- **Storage Engine (`src/store/jsonStore.ts`)**: A singleton (`JsonStore`) that manages the in-memory `JsonData`.
- **Atomic Writes**: Uses a temp-file + rename strategy to ensure file integrity.
- **Concurrency**: Serializes writes via a Promise chain (`saveChain`).
- **Dynamic Router (`src/server/routes/dynamicRouter.ts`)**: Dynamically registers Express routes for every key (collection) in the JSON file.
- **REST Support**: Full CRUD (GET, POST, PUT, PATCH, DELETE).
- **Features**: Filtering, Pagination (`_page`, `_limit`), and smart ID generation (numeric `max+1` or `UUID`).
- **Server Harness (`src/server/app.ts` & `bootstrap.ts`)**:
- **Middleware**: Includes CORS, JSON parsing, request logging, and a custom `delayMiddleware` for simulating latency.

## 🛠 Key Workflows

### Initialization
1. CLI parses arguments.
2. `JsonStore.load(filePath)` reads and validates the JSON structure.
3. `bootstrap(port, options)` starts the Express server.
4. `buildDynamicRouter()` registers routes based on the loaded data keys.

### Data Persistence
Every mutation (POST, PUT, PATCH, DELETE) follows this flow:
1. Update the in-memory `JsonStore.getData()` collection.
2. Call `await JsonStore.save()`.
3. `JsonStore` serializes the write to disk atomically.

### Hot Reloading
When started with `--watch`, `fs.watch` monitors the source file. Any change triggers `JsonStore.load(filePath)`, updating the in-memory state without a server restart.

## 📝 Standards & Conventions
- **Type Safety**: Use explicit type guards (see `src/store/jsonStore.ts` and `src/server/routes/dynamicRouter.ts`) when dealing with user-provided JSON.
- **Persistence**: Always use `JsonStore.save()` after modifying the data returned by `JsonStore.getData()`.
- **Atomic Operations**: Ensure file operations remain atomic to prevent data corruption.
159 changes: 25 additions & 134 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,146 +1,37 @@
# zero-mock
# Frontend Consumer Test

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.8-blue?logo=typescript&logoColor=white)](tsconfig.json)
[![npm version](https://img.shields.io/npm/v/@xirconsss/zero-mock.svg)](https://www.npmjs.com/package/@xirconsss/zero-mock)
This branch tests `zero-mock` as an end-user using the published npm package.

**zero-mock** is a zero-config Node.js CLI that turns a JSON file into a local REST API. Point it at a file whose top-level keys are **collection names** and whose values are **arrays of records**—it serves full CRUD routes and writes changes back to disk. Built for **frontend developers** who need a quick, realistic backend for prototypes, demos, and integration tests without standing up a database or bespoke server.
## How to run

## Demo
1. **Install the CLI globally (or run via npx):**
```bash
npm install -g @xirconsss/zero-mock
```

![Terminal demo](docs/demo.gif)
2. **Start the mock server:**
```bash
npx @xirconsss/zero-mock -- --file ./data.json --port 3000
```

## Installation
With simulated delay (2s) and watch, use **long flags** so `npx`/`npm` never treats `-d` as its own debug flag:

### Global
```bash
npx @xirconsss/zero-mock -- --file ./data.json --port 3000 --delay 2000 --watch
```

```bash
npm install -g @xirconsss/zero-mock
```
If you still see `required option '-f, --file' not specified`, use **`npm exec`** instead:

Run the CLI as **`zero-mock`** (see [Usage](#usage)).
```bash
npm exec -- @xirconsss/zero-mock -- --file ./data.json --port 3000 --delay 2000
```

### One-off with npx
Or install globally and avoid `npx` parsing entirely:

```bash
npx @xirconsss/zero-mock -f ./example/db.json -p 3000
```
```bash
zero-mock --file ./data.json --port 3000 --delay 2000
```

If your shell or npm version forwards extra flags to npm instead of the CLI, insert **`--`** before the first CLI flag (e.g. `npx @xirconsss/zero-mock -- -f ./data.json -p 3000 -w`).
3. **Run the Frontend:**
Simply open `index.html` in your browser.

## Usage

| Flag | Description |
| ---- | ----------- |
| **`-f` / `--file`** | Required. Path to the JSON file. |
| **`-p` / `--port`** | HTTP port (default `3000`, must be 1–65535). |
| **`-d` / `--delay`** | Optional. Delay every request by this many milliseconds. Must be a non-negative integer using digits only (default `0`). |
| **`-w` / `--watch`** | Optional. Watch the JSON file and reload the in-memory data when it changes. If a save produces invalid JSON, the server prints `[watch] Could not reload "<path>": ...` to stderr and keeps the last good data until the file is valid again. Only one reload runs at a time. When watch starts, you also get `[watch] Watching "<path>" for changes.` on stdout. |

Examples:

```bash
zero-mock -f ./data.json -p 3000 -d 200
zero-mock -f ./data.json -w
```

## Quick start

From the repo root (or any directory containing the example file):

```bash
npx @xirconsss/zero-mock -f ./example/db.json -p 3000
```

Optional:

```bash
npx @xirconsss/zero-mock -f ./example/db.json -p 3000 -d 200 -w
```

In another terminal:

```bash
curl http://localhost:3000/users
curl http://localhost:3000/users/1
```

The server logs the exact URLs for each collection when it starts.

## Development

From a clone: `npm install`, then `npm run build` (or `npm run dev` with `ts-node`). Run the built CLI with:

```bash
node dist/index.js -f ./example/db.json -p 3000
```

Add `-d` / `-w` the same way as the published CLI.

## Features

- **Zero-config** — one JSON file defines your API surface; no schemas or generators to run.
- **Full CRUD REST API** — `GET`, `POST`, `PUT`, `PATCH`, and `DELETE` per collection, with CORS and JSON bodies enabled.
- **Atomic file persistence** — writes go through a temp file and rename, with serialized saves so concurrent requests do not corrupt the file.
- **Smart ID generation** — new rows get the next **numeric** id when existing ids are integers or all-digit strings; otherwise new ids use a **UUID**.
- **Request logging** — each finished request logs as `[METHOD] <path> - <status>` (path is Express `req.path`, no query string).
- **Optional delay** — `-d` adds a fixed pause before route handling (after JSON body parsing).
- **List filtering and pagination** — see [List GET](#list-get) on `GET /{resource}`.
- **Watch mode** — `-w` reloads data from disk on file change without restarting the process (see [Usage](#usage)).

## Auto-generated API

Each **top-level key** in your JSON (e.g. `users`, `posts`) becomes a **resource** name. Replace `{resource}` with that key and `{id}` with a row’s `id` (string params match numeric ids loosely).

| Method | Path | Description |
| -------- | -------------------- | ----------- |
| `GET` | `/{resource}` | List items (optionally [filtered and paginated](#list-get)). |
| `POST` | `/{resource}` | Create an item; server assigns `id` and returns `201` with the new body. |
| `GET` | `/{resource}/{id}` | Return one item by `id`; `404` if missing. |
| `PUT` | `/{resource}/{id}` | Replace the item; `id` in the URL wins; `404` if missing. |
| `PATCH` | `/{resource}/{id}` | Shallow-merge fields into the item; `404` if missing. |
| `DELETE` | `/{resource}/{id}` | Remove the item; `204` on success; `404` if missing. |

Invalid JSON bodies (non-objects for write routes) receive **`400`** with a JSON error message. Persistence failures surface as **`500`**.

### List GET

**Filtering:** Every query parameter except `_page` and `_limit` is a filter. Only plain-object rows are kept. For each filter key, the row must have that property, and the value must match the query value with loose equality (`==`). Query values are strings (first value wins if repeated). Rows missing a filter key are dropped.

**Pagination:** If **both** `_page` and `_limit` are present and are positive integers (digit strings only), the list is sliced after filtering. `_page` is 1-based. If either is missing or invalid, the full filtered list is returned (no error).

Examples using [example/db.json](example/db.json):

```bash
curl 'http://localhost:3000/users?role=admin'
curl 'http://localhost:3000/users?_page=1&_limit=2'
```

## JSON file shape

The root must be a JSON **object**. Each property must be an **array** (your “tables”). Each item you want to address by URL should include an **`id`** field (number or string).

## Publishing to npm (maintainers)

**Release flow (recommended):** bump **`version`** in `package.json` and `package-lock.json`, commit, and **push to `main`**. [`.github/workflows/publish-npm.yml`](.github/workflows/publish-npm.yml) runs automatically, builds, and runs **`npm publish`**. If that version is already on the registry, the job skips publish and succeeds with a notice (no E403). You can still trigger a run manually from the **Actions** tab (**workflow_dispatch**). Avoid **`npm publish` on your machine** for the same version CI will publish, or you will block CI with “already published”.

1. Use an [npmjs.com](https://www.npmjs.com/) account with **2FA** enabled and permission to publish the **`@xirconsss`** scope (user or org on npm).
2. **GitHub:** repo → **Settings** → **Environments** → **`NPM_TOKEN`** → add secret **`NPM_TOKEN`** (see token steps below). The workflow uses that environment on each run.
3. Bump **`version`**, push to **`main`**, wait for **Publish to npm** to finish. Check with `npm view @xirconsss/zero-mock version`.

**Token on npm (required for CI):**

1. Create a classic **[Automation](https://docs.npmjs.com/creating-and-viewing-access-tokens#creating-classic-tokens)** token, **or** a **[granular access token](https://docs.npmjs.com/creating-and-viewing-access-tokens#creating-granular-access-tokens)** with **read and write** on **`@xirconsss/zero-mock`** (and org **`xirconsss`** if npm asks).
2. For granular tokens: turn **Bypass two-factor authentication (2FA)** **on** so CI does not hit **`EOTP`**. Do **not** use **`NPM_OTP`** secrets (codes expire in ~30 seconds).
3. Paste the token into the **`NPM_TOKEN`** environment secret on GitHub.

**Optional — OIDC trusted publishing:** You can later move to [npm trusted publishing](https://docs.npmjs.com/trusted-publishers) and drop the secret; if you see **`E404`** on `PUT` with OIDC, the Trusted Publisher settings on npm (repo, workflow filename, environment name) do not match this workflow—token auth avoids that until it is configured correctly.

## License

MIT - see [LICENSE](LICENSE).

## Links

- **Repository:** [github.com/xircons/zero-mock](https://github.com/xircons/zero-mock)
- **Issues:** [github.com/xircons/zero-mock/issues](https://github.com/xircons/zero-mock/issues)
3 changes: 3 additions & 0 deletions commitlint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
extends: ['@commitlint/config-conventional']
};
Loading
Loading