fix: patch error (#1676)

This commit is contained in:
typicode
2026-01-18 00:14:56 +01:00
committed by GitHub
parent 580a71faa6
commit c3afd0fb19
17 changed files with 386 additions and 362 deletions

View File

@@ -10,8 +10,8 @@ jobs:
version: 10
- uses: actions/setup-node@v4
with:
node-version: '22.x'
cache: 'pnpm'
node-version: "22.x"
cache: "pnpm"
- run: pnpm install
- run: pnpm run typecheck
- run: pnpm test

View File

@@ -15,8 +15,8 @@ jobs:
version: 10
- uses: actions/setup-node@v4
with:
node-version: '22.x'
registry-url: 'https://registry.npmjs.org'
node-version: "22.x"
registry-url: "https://registry.npmjs.org"
- run: pnpm install
- run: pnpm publish --provenance --access public
env:

4
.oxfmtrc.json Normal file
View File

@@ -0,0 +1,4 @@
{
"semi": false,
"singleQuote": true,
}

View File

@@ -1,5 +0,0 @@
export default {
semi: false,
singleQuote: true,
trailingComma: 'all',
}

View File

@@ -41,15 +41,15 @@ Create a `db.json` or `db.json5` file
```json5
{
posts: [
{ id: '1', title: 'a title', views: 100 },
{ id: '2', title: 'another title', views: 200 },
{ id: "1", title: "a title", views: 100 },
{ id: "2", title: "another title", views: 200 },
],
comments: [
{ id: '1', text: 'a comment about post 1', postId: '1' },
{ id: '2', text: 'another comment about post 1', postId: '1' },
{ id: "1", text: "a comment about post 1", postId: "1" },
{ id: "2", text: "another comment about post 1", postId: "1" },
],
profile: {
name: 'typicode',
name: "typicode",
},
}
```
@@ -81,22 +81,22 @@ Run `json-server --help` for a list of options
### Gold
||
| :---: |
| <a href="https://mockend.com/" target="_blank"><img src="https://jsonplaceholder.typicode.com/mockend.svg" height="100px"></a> |
| |
| :--------------------------------------------------------------------------------------------------------------------------------------------------------: |
| <a href="https://mockend.com/" target="_blank"><img src="https://jsonplaceholder.typicode.com/mockend.svg" height="100px"></a> |
| <a href="https://zuplo.link/json-server-gh"><img src="https://github.com/user-attachments/assets/adfee31f-a8b6-4684-9a9b-af4f03ac5b75" height="100px"></a> |
| <a href="https://www.mintlify.com/"><img src="https://github.com/user-attachments/assets/bcc8cc48-b2d9-4577-8939-1eb4196b7cc5" height="100px"></a> |
| <a href="https://www.mintlify.com/"><img src="https://github.com/user-attachments/assets/bcc8cc48-b2d9-4577-8939-1eb4196b7cc5" height="100px"></a> |
### Silver
||
| :---: |
| |
| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| <a href="https://requestly.com?utm_source=githubsponsor&utm_medium=jsonserver&utm_campaign=jsonserver"><img src="https://github.com/user-attachments/assets/f7e7b3cf-97e2-46b8-81c8-cb3992662a1c" style="height:70px; width:auto;"></a> |
### Bronze
|||
| :---: | :---: |
| | |
| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| <a href="https://www.storyblok.com/" target="_blank"><img src="https://github.com/typicode/json-server/assets/5502029/c6b10674-4ada-4616-91b8-59d30046b45a" height="35px"></a> | <a href="https://betterstack.com/" target="_blank"><img src="https://github.com/typicode/json-server/assets/5502029/44679f8f-9671-470d-b77e-26d90b90cbdc" height="35px"></a> |
[Become a sponsor and have your company logo here](https://github.com/users/typicode/sponsorship)
@@ -104,7 +104,7 @@ Run `json-server --help` for a list of options
## Sponsorware
> [!NOTE]
> This project uses the [Fair Source License](https://fair.io/). Only organizations with 3+ users are kindly asked to contribute a small amount through sponsorship [sponsor](https://github.com/sponsors/typicode) for usage. __This license helps keep the project sustainable and healthy, benefiting everyone.__
> This project uses the [Fair Source License](https://fair.io/). Only organizations with 3+ users are kindly asked to contribute a small amount through sponsorship [sponsor](https://github.com/sponsors/typicode) for usage. **This license helps keep the project sustainable and healthy, benefiting everyone.**
>
> For more information, FAQs, and the rationale behind this, visit [https://fair.io/](https://fair.io/).

View File

@@ -10,4 +10,4 @@
"profile": {
"name": "typicode"
}
}
}

View File

@@ -1,27 +1,27 @@
{
posts: [
{
id: '1',
title: 'a title',
id: "1",
title: "a title",
},
{
id: '2',
title: 'another title',
id: "2",
title: "another title",
},
],
comments: [
{
id: '1',
text: 'a comment about post 1',
postId: '1',
id: "1",
text: "a comment about post 1",
postId: "1",
},
{
id: '2',
text: 'another comment about post 1',
postId: '1',
id: "2",
text: "another comment about post 1",
postId: "1",
},
],
profile: {
name: 'typicode',
name: "typicode",
},
}
}

View File

@@ -2,7 +2,13 @@
"name": "json-server",
"version": "1.0.0-beta.3",
"description": "",
"type": "module",
"keywords": [],
"license": "SEE LICENSE IN ./LICENSE",
"author": "typicode <typicode@gmail.com>",
"repository": {
"type": "git",
"url": "git+https://github.com/typicode/json-server.git"
},
"bin": {
"json-server": "lib/bin.js"
},
@@ -10,9 +16,7 @@
"lib",
"views"
],
"engines": {
"node": ">=22.12.0"
},
"type": "module",
"scripts": {
"dev": "node --watch --experimental-strip-types src/bin.ts fixtures/db.json",
"build": "rm -rf lib && tsc",
@@ -20,25 +24,10 @@
"typecheck": "tsc --noEmit",
"test": "node --experimental-strip-types --test src/*.test.ts",
"lint": "oxlint src",
"fmt": "oxfmt",
"fmt:check": "oxfmt --check",
"prepare": "husky"
},
"keywords": [],
"author": "typicode <typicode@gmail.com>",
"license": "SEE LICENSE IN ./LICENSE",
"repository": {
"type": "git",
"url": "git+https://github.com/typicode/json-server.git"
},
"devDependencies": {
"@types/node": "^25.0.8",
"concurrently": "^9.2.1",
"get-port": "^7.1.0",
"husky": "^9.1.7",
"oxlint": "^1.39.0",
"tempy": "^3.1.0",
"type-fest": "^5.4.0",
"typescript": "^5.9.3"
},
"dependencies": {
"@tinyhttp/app": "^3.0.1",
"@tinyhttp/cors": "^2.0.1",
@@ -53,5 +42,19 @@
"milliparsec": "^5.1.0",
"sirv": "^3.0.2",
"sort-on": "^7.0.0"
},
"devDependencies": {
"@types/node": "^25.0.8",
"concurrently": "^9.2.1",
"get-port": "^7.1.0",
"husky": "^9.1.7",
"oxfmt": "^0.24.0",
"oxlint": "^1.39.0",
"tempy": "^3.1.0",
"type-fest": "^5.4.0",
"typescript": "^5.9.3"
},
"engines": {
"node": ">=22.12.0"
}
}

95
pnpm-lock.yaml generated
View File

@@ -60,6 +60,9 @@ importers:
husky:
specifier: ^9.1.7
version: 9.1.7
oxfmt:
specifier: ^0.24.0
version: 0.24.0
oxlint:
specifier: ^1.39.0
version: 1.39.0
@@ -75,6 +78,50 @@ importers:
packages:
'@oxfmt/darwin-arm64@0.24.0':
resolution: {integrity: sha512-aYXuGf/yq8nsyEcHindGhiz9I+GEqLkVq8CfPbd+6VE259CpPEH+CaGHEO1j6vIOmNr8KHRq+IAjeRO2uJpb8A==}
cpu: [arm64]
os: [darwin]
'@oxfmt/darwin-x64@0.24.0':
resolution: {integrity: sha512-vs3b8Bs53hbiNvcNeBilzE/+IhDTWKjSBB3v/ztr664nZk65j0xr+5IHMBNz3CFppmX7o/aBta2PxY+t+4KoPg==}
cpu: [x64]
os: [darwin]
'@oxfmt/linux-arm64-gnu@0.24.0':
resolution: {integrity: sha512-ItPDOPoQ0wLj/s8osc5ch57uUcA1Wk8r0YdO8vLRpXA3UNg7KPOm1vdbkIZRRiSUphZcuX5ioOEetEK8H7RlTw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxfmt/linux-arm64-musl@0.24.0':
resolution: {integrity: sha512-JkQO3WnQjQTJONx8nxdgVBfl6BBFfpp9bKhChYhWeakwJdr7QPOAWJ/v3FGZfr0TbqINwnNR74aVZayDDRyXEA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxfmt/linux-x64-gnu@0.24.0':
resolution: {integrity: sha512-N/SXlFO+2kak5gMt0oxApi0WXQDhwA0PShR0UbkY0PwtHjfSiDqJSOumyNqgQVoroKr1GNnoRmUqjZIz6DKIcw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxfmt/linux-x64-musl@0.24.0':
resolution: {integrity: sha512-WM0pek5YDCQf50XQ7GLCE9sMBCMPW/NPAEPH/Hx6Qyir37lEsP4rUmSECo/QFNTU6KBc9NnsviAyJruWPpCMXw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxfmt/win32-arm64@0.24.0':
resolution: {integrity: sha512-vFCseli1KWtwdHrVlT/jWfZ8jP8oYpnPPEjI23mPLW8K/6GEJmmvy0PZP5NpWUFNTzX0lqie58XnrATJYAe9Xw==}
cpu: [arm64]
os: [win32]
'@oxfmt/win32-x64@0.24.0':
resolution: {integrity: sha512-0tmlNzcyewAnauNeBCq0xmAkmiKzl+H09p0IdHy+QKrTQdtixtf+AOjDAADbRfihkS+heF15Pjc4IyJMdAAJjw==}
cpu: [x64]
os: [win32]
'@oxlint/darwin-arm64@1.39.0':
resolution: {integrity: sha512-lT3hNhIa02xCujI6YGgjmYGg3Ht/X9ag5ipUVETaMpx5Rd4BbTNWUPif1WN1YZHxt3KLCIqaAe7zVhatv83HOQ==}
cpu: [arm64]
@@ -332,6 +379,11 @@ packages:
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
engines: {node: '>= 0.6'}
oxfmt@0.24.0:
resolution: {integrity: sha512-UjeM3Peez8Tl7IJ9s5UwAoZSiDRMww7BEc21gDYxLq3S3/KqJnM3mjNxsoSHgmBvSeX6RBhoVc2MfC/+96RdSw==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
oxlint@1.39.0:
resolution: {integrity: sha512-wSiLr0wjG+KTU6c1LpVoQk7JZ7l8HCKlAkVDVTJKWmCGazsNxexxnOXl7dsar92mQcRnzko5g077ggP3RINSjA==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -401,6 +453,10 @@ packages:
resolution: {integrity: sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==}
engines: {node: '>=14.16'}
tinypool@2.0.0:
resolution: {integrity: sha512-/RX9RzeH2xU5ADE7n2Ykvmi9ED3FBGPAjw9u3zucrNNaEBIO0HPSYgL0NT7+3p147ojeSdaVu08F6hjpv31HJg==}
engines: {node: ^20.0.0 || >=22.0.0}
totalist@3.0.1:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'}
@@ -454,6 +510,30 @@ packages:
snapshots:
'@oxfmt/darwin-arm64@0.24.0':
optional: true
'@oxfmt/darwin-x64@0.24.0':
optional: true
'@oxfmt/linux-arm64-gnu@0.24.0':
optional: true
'@oxfmt/linux-arm64-musl@0.24.0':
optional: true
'@oxfmt/linux-x64-gnu@0.24.0':
optional: true
'@oxfmt/linux-x64-musl@0.24.0':
optional: true
'@oxfmt/win32-arm64@0.24.0':
optional: true
'@oxfmt/win32-x64@0.24.0':
optional: true
'@oxlint/darwin-arm64@1.39.0':
optional: true
@@ -656,6 +736,19 @@ snapshots:
negotiator@1.0.0: {}
oxfmt@0.24.0:
dependencies:
tinypool: 2.0.0
optionalDependencies:
'@oxfmt/darwin-arm64': 0.24.0
'@oxfmt/darwin-x64': 0.24.0
'@oxfmt/linux-arm64-gnu': 0.24.0
'@oxfmt/linux-arm64-musl': 0.24.0
'@oxfmt/linux-x64-gnu': 0.24.0
'@oxfmt/linux-x64-musl': 0.24.0
'@oxfmt/win32-arm64': 0.24.0
'@oxfmt/win32-x64': 0.24.0
oxlint@1.39.0:
optionalDependencies:
'@oxlint/darwin-arm64': 1.39.0
@@ -720,6 +813,8 @@ snapshots:
type-fest: 2.19.0
unique-string: 3.0.0
tinypool@2.0.0: {}
totalist@3.0.1: {}
tree-kill@1.2.2: {}

View File

@@ -1 +1 @@
<!-- Testing automatic serving of files from the 'public/' directory -->
<!-- Testing automatic serving of files from the 'public/' directory -->

View File

@@ -1,4 +1,4 @@
import assert from 'node:assert/strict'
import assert from 'node:assert/strict'
import { writeFileSync } from 'node:fs'
import { join } from 'node:path'
import test from 'node:test'
@@ -11,20 +11,12 @@ import { createApp } from './app.ts'
import type { Data } from './service.ts'
type Test = {
method: HTTPMethods
url: string
statusCode: number
}
type HTTPMethods =
| 'DELETE'
| 'GET'
| 'HEAD'
| 'PATCH'
| 'POST'
| 'PUT'
| 'OPTIONS'
type HTTPMethods = 'DELETE' | 'GET' | 'HEAD' | 'PATCH' | 'POST' | 'PUT' | 'OPTIONS'
const port = await getPort()

View File

@@ -54,9 +54,7 @@ export function createApp(db: Low<Data>, options: AppOptions = {}) {
// Body parser
app.use(json())
app.get('/', (_req, res) =>
res.send(eta.render('index.html', { data: db.data })),
)
app.get('/', (_req, res) => res.send(eta.render('index.html', { data: db.data })))
app.get('/:name', (req, res, next) => {
const { name = '' } = req.params
@@ -69,11 +67,11 @@ export function createApp(db: Low<Data>, options: AppOptions = {}) {
['_start', '_end', '_limit', '_page', '_per_page'].includes(key) &&
typeof value === 'string'
) {
value = parseInt(value);
value = parseInt(value)
}
if (!Number.isNaN(value)) {
query[key] = value;
query[key] = value
}
})
res.locals['data'] = service.find(name, query)
@@ -128,11 +126,7 @@ export function createApp(db: Low<Data>, options: AppOptions = {}) {
app.delete('/:name/:id', async (req, res, next) => {
const { name = '', id = '' } = req.params
res.locals['data'] = await service.destroyById(
name,
id,
req.query['_dependent'],
)
res.locals['data'] = await service.destroyById(name, id, req.query['_dependent'])
next?.()
})

View File

@@ -1,20 +1,20 @@
#!/usr/bin/env node
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import { extname } from 'node:path'
import { parseArgs } from 'node:util'
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { extname } from "node:path";
import { parseArgs } from "node:util";
import chalk from 'chalk'
import { watch } from 'chokidar'
import JSON5 from 'json5'
import { Low } from 'lowdb'
import type { Adapter } from 'lowdb'
import { DataFile, JSONFile } from 'lowdb/node'
import type { PackageJson } from 'type-fest'
import chalk from "chalk";
import { watch } from "chokidar";
import JSON5 from "json5";
import { Low } from "lowdb";
import type { Adapter } from "lowdb";
import { DataFile, JSONFile } from "lowdb/node";
import type { PackageJson } from "type-fest";
import { fileURLToPath } from 'node:url'
import { createApp } from './app.ts'
import { Observer } from './observer.ts'
import type { Data } from './service.ts'
import { fileURLToPath } from "node:url";
import { createApp } from "./app.ts";
import { Observer } from "./observer.ts";
import type { Data } from "./service.ts";
function help() {
console.log(`Usage: json-server [options] <file>
@@ -25,204 +25,195 @@ Options:
-s, --static <dir> Static files directory (multiple allowed)
--help Show this message
--version Show version number
`)
`);
}
// Parse args
function args(): {
file: string
port: number
host: string
static: string[]
file: string;
port: number;
host: string;
static: string[];
} {
try {
const { values, positionals } = parseArgs({
options: {
port: {
type: 'string',
short: 'p',
default: process.env['PORT'] ?? '3000',
type: "string",
short: "p",
default: process.env["PORT"] ?? "3000",
},
host: {
type: 'string',
short: 'h',
default: process.env['HOST'] ?? 'localhost',
type: "string",
short: "h",
default: process.env["HOST"] ?? "localhost",
},
static: {
type: 'string',
short: 's',
type: "string",
short: "s",
multiple: true,
default: [],
},
help: {
type: 'boolean',
type: "boolean",
},
version: {
type: 'boolean',
type: "boolean",
},
// Deprecated
watch: {
type: 'boolean',
short: 'w',
type: "boolean",
short: "w",
},
},
allowPositionals: true,
})
});
// --version
if (values.version) {
const pkg = JSON.parse(
readFileSync(
fileURLToPath(new URL('../package.json', import.meta.url)),
'utf-8',
),
) as PackageJson
console.log(pkg.version)
process.exit()
readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf-8"),
) as PackageJson;
console.log(pkg.version);
process.exit();
}
// Handle --watch
if (values.watch) {
console.log(
chalk.yellow(
'--watch/-w can be omitted, JSON Server 1+ watches for file changes by default',
"--watch/-w can be omitted, JSON Server 1+ watches for file changes by default",
),
)
);
}
if (values.help || positionals.length === 0) {
help()
process.exit()
help();
process.exit();
}
// App args and options
return {
file: positionals[0] ?? '',
file: positionals[0] ?? "",
port: parseInt(values.port as string),
host: values.host as string,
static: values.static as string[],
}
};
} catch (e) {
if ((e as NodeJS.ErrnoException).code === 'ERR_PARSE_ARGS_UNKNOWN_OPTION') {
console.log(chalk.red((e as NodeJS.ErrnoException).message.split('.')[0]))
help()
process.exit(1)
if ((e as NodeJS.ErrnoException).code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") {
console.log(chalk.red((e as NodeJS.ErrnoException).message.split(".")[0]));
help();
process.exit(1);
} else {
throw e
throw e;
}
}
}
const { file, port, host, static: staticArr } = args()
const { file, port, host, static: staticArr } = args();
if (!existsSync(file)) {
console.log(chalk.red(`File ${file} not found`))
process.exit(1)
console.log(chalk.red(`File ${file} not found`));
process.exit(1);
}
// Handle empty string JSON file
if (readFileSync(file, 'utf-8').trim() === '') {
writeFileSync(file, '{}')
if (readFileSync(file, "utf-8").trim() === "") {
writeFileSync(file, "{}");
}
// Set up database
let adapter: Adapter<Data>
if (extname(file) === '.json5') {
let adapter: Adapter<Data>;
if (extname(file) === ".json5") {
adapter = new DataFile<Data>(file, {
parse: JSON5.parse,
stringify: JSON5.stringify,
})
});
} else {
adapter = new JSONFile<Data>(file)
adapter = new JSONFile<Data>(file);
}
const observer = new Observer(adapter)
const observer = new Observer(adapter);
const db = new Low<Data>(observer, {})
await db.read()
const db = new Low<Data>(observer, {});
await db.read();
// Create app
const app = createApp(db, { logger: false, static: staticArr })
const app = createApp(db, { logger: false, static: staticArr });
function logRoutes(data: Data) {
console.log(chalk.bold('Endpoints:'))
console.log(chalk.bold("Endpoints:"));
if (Object.keys(data).length === 0) {
console.log(
chalk.gray(`No endpoints found, try adding some data to ${file}`),
)
return
console.log(chalk.gray(`No endpoints found, try adding some data to ${file}`));
return;
}
console.log(
Object.keys(data)
.map(
(key) => `${chalk.gray(`http://${host}:${port}/`)}${chalk.blue(key)}`,
)
.join('\n'),
)
.map((key) => `${chalk.gray(`http://${host}:${port}/`)}${chalk.blue(key)}`)
.join("\n"),
);
}
const kaomojis = ['♡⸜(˶˃ ᵕ ˂˶)⸝♡', '♡( ◡‿◡ )', '( ˶ˆ ᗜ ˆ˵ )', '(˶ᵔ ᵕ ᵔ˶)']
const kaomojis = ["♡⸜(˶˃ ᵕ ˂˶)⸝♡", "♡( ◡‿◡ )", "( ˶ˆ ᗜ ˆ˵ )", "(˶ᵔ ᵕ ᵔ˶)"];
function randomItem(items: string[]): string {
const index = Math.floor(Math.random() * items.length)
return items.at(index) ?? ''
const index = Math.floor(Math.random() * items.length);
return items.at(index) ?? "";
}
app.listen(port, () => {
console.log(
[
chalk.bold(`JSON Server started on PORT :${port}`),
chalk.gray('Press CTRL-C to stop'),
chalk.gray("Press CTRL-C to stop"),
chalk.gray(`Watching ${file}...`),
'',
"",
chalk.magenta(randomItem(kaomojis)),
'',
chalk.bold('Index:'),
"",
chalk.bold("Index:"),
chalk.gray(`http://localhost:${port}/`),
'',
chalk.bold('Static files:'),
chalk.gray('Serving ./public directory if it exists'),
'',
].join('\n'),
)
logRoutes(db.data)
})
"",
chalk.bold("Static files:"),
chalk.gray("Serving ./public directory if it exists"),
"",
].join("\n"),
);
logRoutes(db.data);
});
// Watch file for changes
if (process.env['NODE_ENV'] !== 'production') {
let writing = false // true if the file is being written to by the app
let prevEndpoints = ''
if (process.env["NODE_ENV"] !== "production") {
let writing = false; // true if the file is being written to by the app
let prevEndpoints = "";
observer.onWriteStart = () => {
writing = true
}
writing = true;
};
observer.onWriteEnd = () => {
writing = false
}
writing = false;
};
observer.onReadStart = () => {
prevEndpoints = JSON.stringify(Object.keys(db.data).sort())
}
prevEndpoints = JSON.stringify(Object.keys(db.data).sort());
};
observer.onReadEnd = (data) => {
if (data === null) {
return
return;
}
const nextEndpoints = JSON.stringify(Object.keys(data).sort())
const nextEndpoints = JSON.stringify(Object.keys(data).sort());
if (prevEndpoints !== nextEndpoints) {
console.log()
logRoutes(data)
console.log();
logRoutes(data);
}
}
watch(file).on('change', () => {
};
watch(file).on("change", () => {
// Do no reload if the file is being written to by the app
if (!writing) {
db.read().catch((e) => {
if (e instanceof SyntaxError) {
return console.log(
chalk.red(['', `Error parsing ${file}`, e.message].join('\n')),
)
return console.log(chalk.red(["", `Error parsing ${file}`, e.message].join("\n")));
}
console.log(e)
})
console.log(e);
});
}
})
});
}

View File

@@ -1,36 +1,36 @@
import type { Adapter } from 'lowdb'
import type { Adapter } from "lowdb";
// Lowdb adapter to observe read/write events
export class Observer<T> {
#adapter
#adapter;
onReadStart = function () {
return
}
return;
};
onReadEnd: (data: T | null) => void = function () {
return
}
return;
};
onWriteStart = function () {
return
}
return;
};
onWriteEnd = function () {
return
}
return;
};
constructor(adapter: Adapter<T>) {
this.#adapter = adapter
this.#adapter = adapter;
}
async read() {
this.onReadStart()
const data = await this.#adapter.read()
this.onReadEnd(data)
return data
this.onReadStart();
const data = await this.#adapter.read();
this.onReadEnd(data);
return data;
}
async write(arg: T) {
this.onWriteStart()
await this.#adapter.write(arg)
this.onWriteEnd()
this.onWriteStart();
await this.#adapter.write(arg);
this.onWriteEnd();
}
}

View File

@@ -77,8 +77,7 @@ await test('constructor', () => {
await test('findById', () => {
reset()
if (!Array.isArray(db.data?.[POSTS]))
throw new Error('posts should be an array')
if (!Array.isArray(db.data?.[POSTS])) throw new Error('posts should be an array')
assert.deepEqual(service.findById(POSTS, '1', {}), db.data?.[POSTS]?.[0])
assert.equal(service.findById(POSTS, UNKNOWN_ID, {}), undefined)
assert.deepEqual(service.findById(POSTS, '1', { _embed: ['comments'] }), {
@@ -339,11 +338,21 @@ await test('update', async () => {
undefined,
'should ignore unknown resources',
)
assert.equal(await service.update(POSTS, {}), undefined, 'should ignore arrays')
})
await test('patch', async () => {
reset()
const obj = { f2: 'bar' }
const res = await service.patch(OBJECT, obj)
assert.deepEqual(res, { f1: 'foo', ...obj })
assert.equal(
await service.update(POSTS, {}),
await service.patch(UNKNOWN_RESOURCE, obj),
undefined,
'should ignore arrays',
'should ignore unknown resources',
)
assert.equal(await service.patch(POSTS, {}), undefined, 'should ignore arrays')
})
await test('updateById', async () => {
@@ -353,10 +362,7 @@ await test('updateById', async () => {
assert.equal(res?.['id'], post1.id, 'id should not change')
assert.equal(res?.['title'], post.title)
assert.equal(
await service.updateById(UNKNOWN_RESOURCE, post1.id, post),
undefined,
)
assert.equal(await service.updateById(UNKNOWN_RESOURCE, post1.id, post), undefined)
assert.equal(await service.updateById(POSTS, UNKNOWN_ID, post), undefined)
})
@@ -368,10 +374,7 @@ await test('patchById', async () => {
assert.equal(res?.['id'], post1.id)
assert.equal(res?.['title'], post.title)
assert.equal(
await service.patchById(UNKNOWN_RESOURCE, post1.id, post),
undefined,
)
assert.equal(await service.patchById(UNKNOWN_RESOURCE, post1.id, post), undefined)
assert.equal(await service.patchById(POSTS, UNKNOWN_ID, post), undefined)
})

View File

@@ -19,9 +19,7 @@ export function isData(obj: unknown): obj is Record<string, Item[]> {
}
const data = obj as Record<string, unknown>
return Object.values(data).every(
(value) => Array.isArray(value) && value.every(isItem),
)
return Object.values(data).every((value) => Array.isArray(value) && value.every(isItem))
}
const Condition = {
@@ -33,7 +31,7 @@ const Condition = {
default: '',
} as const
type Condition = typeof Condition[keyof typeof Condition]
type Condition = (typeof Condition)[keyof typeof Condition]
function isCondition(value: string): value is Condition {
return Object.values<string>(Condition).includes(value)
@@ -151,11 +149,7 @@ export class Service {
return Object.prototype.hasOwnProperty.call(this.#db?.data, name)
}
findById(
name: string,
id: string,
query: { _embed?: string[] | string },
): Item | undefined {
findById(name: string, id: string, query: { _embed?: string[] | string }): Item | undefined {
const value = this.#get(name)
if (Array.isArray(value)) {
@@ -214,17 +208,7 @@ export class Service {
conds.push([field, op, value])
continue
}
if (
[
'_embed',
'_sort',
'_start',
'_end',
'_limit',
'_page',
'_per_page',
].includes(key)
) {
if (['_embed', '_sort', '_start', '_end', '_limit', '_page', '_per_page'].includes(key)) {
continue
}
conds.push([key, Condition.default, value])
@@ -240,48 +224,28 @@ export class Service {
switch (op) {
// item_gt=value
case Condition.gt: {
if (
!(
typeof itemValue === 'number' &&
itemValue > parseInt(paramValue)
)
) {
if (!(typeof itemValue === 'number' && itemValue > parseInt(paramValue))) {
return false
}
break
}
// item_gte=value
case Condition.gte: {
if (
!(
typeof itemValue === 'number' &&
itemValue >= parseInt(paramValue)
)
) {
if (!(typeof itemValue === 'number' && itemValue >= parseInt(paramValue))) {
return false
}
break
}
// item_lt=value
case Condition.lt: {
if (
!(
typeof itemValue === 'number' &&
itemValue < parseInt(paramValue)
)
) {
if (!(typeof itemValue === 'number' && itemValue < parseInt(paramValue))) {
return false
}
break
}
// item_lte=value
case Condition.lte: {
if (
!(
typeof itemValue === 'number' &&
itemValue <= parseInt(paramValue)
)
) {
if (!(typeof itemValue === 'number' && itemValue <= parseInt(paramValue))) {
return false
}
break
@@ -368,10 +332,7 @@ export class Service {
return sorted.slice(start, end)
}
async create(
name: string,
data: Omit<Item, 'id'> = {},
): Promise<Item | undefined> {
async create(name: string, data: Omit<Item, 'id'> = {}): Promise<Item | undefined> {
const items = this.#get(name)
if (items === undefined || !Array.isArray(items)) return
@@ -382,15 +343,11 @@ export class Service {
return item
}
async #updateOrPatch(
name: string,
body: Item = {},
isPatch: boolean,
): Promise<Item | undefined> {
async #updateOrPatch(name: string, body: Item = {}, isPatch: boolean): Promise<Item | undefined> {
const item = this.#get(name)
if (item === undefined || Array.isArray(item)) return
const nextItem = (this.#db.data[name] = isPatch ? { item, ...body } : body)
const nextItem = (this.#db.data[name] = isPatch ? { ...item, ...body } : body)
await this.#db.write()
return nextItem
@@ -424,19 +381,11 @@ export class Service {
return this.#updateOrPatch(name, body, true)
}
async updateById(
name: string,
id: string,
body: Item = {},
): Promise<Item | undefined> {
async updateById(name: string, id: string, body: Item = {}): Promise<Item | undefined> {
return this.#updateOrPatchById(name, id, body, false)
}
async patchById(
name: string,
id: string,
body: Item = {},
): Promise<Item | undefined> {
async patchById(name: string, id: string, body: Item = {}): Promise<Item | undefined> {
return this.#updateOrPatchById(name, id, body, true)
}

View File

@@ -1,97 +1,95 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
html {
font-size: 16px;
line-height: 1.5;
background-color: #fff;
color: #000;
}
body {
margin: 0 auto;
max-width: 720px;
padding: 0 16px;
font-family: sans-serif;
}
a {
color: #db2777;
text-decoration: none;
}
header {
margin-bottom: 32px;
padding: 16px 0;
}
nav {
display: flex;
justify-content: space-between;
}
nav div a {
margin-left: 16px;
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
li {
margin-bottom: 8px;
}
/* Dark mode styles */
@media (prefers-color-scheme: dark) {
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
html {
background-color: #1e293b;
color: #fff;
font-size: 16px;
line-height: 1.5;
background-color: #fff;
color: #000;
}
body {
margin: 0 auto;
max-width: 720px;
padding: 0 16px;
font-family: sans-serif;
}
a {
color: #db2777;
text-decoration: none;
}
}
</style>
</head>
header {
margin-bottom: 32px;
padding: 16px 0;
}
<body>
<header>
<nav>
<strong>JSON Server</strong>
<div>
<a href="https://github.com/typicode/json-server">Docs</a>
<a href="https://github.com/sponsors/typicode">♡ Sponsor</a>
</div>
</nav>
</header>
<main class="my-12">
<p class="bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 text-transparent bg-clip-text">✧*。٩(ˊᗜˋ*)و✧*。</p>
<% if (Object.keys(it.data).length===0) { %>
nav {
display: flex;
justify-content: space-between;
}
nav div a {
margin-left: 16px;
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
li {
margin-bottom: 8px;
}
/* Dark mode styles */
@media (prefers-color-scheme: dark) {
html {
background-color: #1e293b;
color: #fff;
}
a {
}
}
</style>
</head>
<body>
<header>
<nav>
<strong>JSON Server</strong>
<div>
<a href="https://github.com/typicode/json-server">Docs</a>
<a href="https://github.com/sponsors/typicode">♡ Sponsor</a>
</div>
</nav>
</header>
<main class="my-12">
<p
class="bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 text-transparent bg-clip-text"
>
✧*。٩(ˊᗜˋ*)و✧*。
</p>
<% if (Object.keys(it.data).length===0) { %>
<p>No resources found in JSON file</p>
<% } %>
<% Object.entries(it.data).forEach(function([name]) { %>
<% } %> <% Object.entries(it.data).forEach(function([name]) { %>
<ul>
<li>
<a href="<%= name %>">/<%= name %></a>
<span>
<% if (Array.isArray(it.data[name])) { %>
- <%= it.data[name].length %>
<%= it.data[name].length> 1 ? 'items' : 'item' %>
<% if (Array.isArray(it.data[name])) { %> - <%= it.data[name].length %> <%=
it.data[name].length> 1 ? 'items' : 'item' %>
</span>
<% } %>
</li>
</ul>
<% }) %>
</main>
</body>
</html>
<% }) %>
</main>
</body>
</html>