json-server 1.0.0-alpha.1 → 1.0.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -4
- package/lib/JSON5File.d.ts +9 -0
- package/lib/JSON5File.js +20 -0
- package/lib/Observer.d.ts +11 -0
- package/lib/Observer.js +30 -0
- package/lib/app.d.ts +8 -0
- package/lib/app.js +75 -0
- package/lib/app.test.d.ts +1 -0
- package/lib/app.test.js +73 -0
- package/lib/bin.d.ts +2 -0
- package/lib/bin.js +99 -0
- package/lib/service.d.ts +39 -0
- package/lib/service.js +270 -0
- package/lib/service.test.d.ts +1 -0
- package/lib/service.test.js +251 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/package.json +8 -4
- package/public/output.css +1086 -0
package/README.md
CHANGED
|
@@ -12,21 +12,29 @@ npm install json-server
|
|
|
12
12
|
|
|
13
13
|
Create a `db.json` file or run `json-server db.json` to create one with some default resources
|
|
14
14
|
|
|
15
|
+
> [!TIP]
|
|
16
|
+
> You can also use [json5](https://json5.org/) format by creating a `db.json5` instead
|
|
17
|
+
|
|
15
18
|
```json
|
|
16
19
|
{
|
|
17
20
|
"posts": [
|
|
18
|
-
{ "id": "1", "title": "
|
|
19
|
-
{ "id": "2", "title": "
|
|
21
|
+
{ "id": "1", "title": "a title" },
|
|
22
|
+
{ "id": "2", "title": "another title" }
|
|
20
23
|
],
|
|
21
24
|
"comments": [
|
|
22
|
-
{ "id": "1", "text": "
|
|
23
|
-
{ "id": "2", "text": "
|
|
25
|
+
{ "id": "1", "text": "a comment about post 1", "postId": "1" },
|
|
26
|
+
{ "id": "2", "text": "another comment about post 1", "postId": "1" }
|
|
24
27
|
]
|
|
25
28
|
}
|
|
26
29
|
```
|
|
27
30
|
|
|
28
31
|
```shell
|
|
29
32
|
json-server db.json
|
|
33
|
+
curl -H "Accept: application/json" -X GET http://localhost:3000/posts/1
|
|
34
|
+
{
|
|
35
|
+
"id": "1",
|
|
36
|
+
"title": "a title"
|
|
37
|
+
}
|
|
30
38
|
```
|
|
31
39
|
|
|
32
40
|
Run `json-server --help` for a list of options
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
import { PathLike } from 'fs';
|
|
3
|
+
import { Adapter } from 'lowdb';
|
|
4
|
+
export declare class JSON5File<T> implements Adapter<T> {
|
|
5
|
+
#private;
|
|
6
|
+
constructor(filename: PathLike);
|
|
7
|
+
read(): Promise<T | null>;
|
|
8
|
+
write(obj: T): Promise<void>;
|
|
9
|
+
}
|
package/lib/JSON5File.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import JSON5 from 'json5';
|
|
2
|
+
import { TextFile } from 'lowdb/node';
|
|
3
|
+
export class JSON5File {
|
|
4
|
+
#adapter;
|
|
5
|
+
constructor(filename) {
|
|
6
|
+
this.#adapter = new TextFile(filename);
|
|
7
|
+
}
|
|
8
|
+
async read() {
|
|
9
|
+
const data = await this.#adapter.read();
|
|
10
|
+
if (data === null) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
return JSON5.parse(data);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
write(obj) {
|
|
18
|
+
return this.#adapter.write(JSON5.stringify(obj, null, 2));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Adapter } from 'lowdb';
|
|
2
|
+
export declare class Observer<T> {
|
|
3
|
+
#private;
|
|
4
|
+
onReadStart: () => void;
|
|
5
|
+
onReadEnd: () => void;
|
|
6
|
+
onWriteStart: () => void;
|
|
7
|
+
onWriteEnd: () => void;
|
|
8
|
+
constructor(adapter: Adapter<T>);
|
|
9
|
+
read(): Promise<T | null>;
|
|
10
|
+
write(arg: T): Promise<void>;
|
|
11
|
+
}
|
package/lib/Observer.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Lowdb adapter to observe read/write events
|
|
2
|
+
export class Observer {
|
|
3
|
+
#adapter;
|
|
4
|
+
onReadStart = function () {
|
|
5
|
+
return;
|
|
6
|
+
};
|
|
7
|
+
onReadEnd = function () {
|
|
8
|
+
return;
|
|
9
|
+
};
|
|
10
|
+
onWriteStart = function () {
|
|
11
|
+
return;
|
|
12
|
+
};
|
|
13
|
+
onWriteEnd = function () {
|
|
14
|
+
return;
|
|
15
|
+
};
|
|
16
|
+
constructor(adapter) {
|
|
17
|
+
this.#adapter = adapter;
|
|
18
|
+
}
|
|
19
|
+
async read() {
|
|
20
|
+
this.onReadStart();
|
|
21
|
+
const data = await this.#adapter.read();
|
|
22
|
+
this.onReadEnd();
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
async write(arg) {
|
|
26
|
+
this.onWriteStart();
|
|
27
|
+
await this.#adapter.write(arg);
|
|
28
|
+
this.onWriteEnd();
|
|
29
|
+
}
|
|
30
|
+
}
|
package/lib/app.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { App } from '@tinyhttp/app';
|
|
2
|
+
import { Low } from 'lowdb';
|
|
3
|
+
import { Data } from './service.js';
|
|
4
|
+
export type AppOptions = {
|
|
5
|
+
logger?: boolean;
|
|
6
|
+
static?: string[];
|
|
7
|
+
};
|
|
8
|
+
export declare function createApp(db: Low<Data>, options?: AppOptions): App<import("@tinyhttp/app").Request, import("@tinyhttp/app").Response<unknown>>;
|
package/lib/app.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { App } from '@tinyhttp/app';
|
|
4
|
+
import { Eta } from 'eta';
|
|
5
|
+
import { json } from 'milliparsec';
|
|
6
|
+
import sirv from 'sirv';
|
|
7
|
+
import { isItem, Service } from './service.js';
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const isProduction = process.env['NODE_ENV'] === 'production';
|
|
10
|
+
const eta = new Eta({
|
|
11
|
+
views: join(__dirname, '../views'),
|
|
12
|
+
cache: isProduction,
|
|
13
|
+
});
|
|
14
|
+
export function createApp(db, options = {}) {
|
|
15
|
+
// Create service
|
|
16
|
+
const service = new Service(db);
|
|
17
|
+
// Create app
|
|
18
|
+
const app = new App();
|
|
19
|
+
// Body parser
|
|
20
|
+
app.use(json());
|
|
21
|
+
// Static files
|
|
22
|
+
app.use(sirv(join(__dirname, '../public')));
|
|
23
|
+
options.static
|
|
24
|
+
?.map((path) => (isAbsolute(path) ? path : join(process.cwd(), path)))
|
|
25
|
+
.forEach((dir) => app.use(sirv(dir, { dev: !isProduction })));
|
|
26
|
+
app.get('/', (_req, res) => res.send(eta.render('index.html', { data: db.data })));
|
|
27
|
+
app.get('/:name', (req, res, next) => {
|
|
28
|
+
const { name = '' } = req.params;
|
|
29
|
+
res.locals['data'] = service.find(name, req.query);
|
|
30
|
+
next();
|
|
31
|
+
});
|
|
32
|
+
app.get('/:name/:id', (req, res, next) => {
|
|
33
|
+
const { name = '', id = '' } = req.params;
|
|
34
|
+
res.locals['data'] = service.findById(name, id, req.query);
|
|
35
|
+
next();
|
|
36
|
+
});
|
|
37
|
+
app.post('/:name', async (req, res, next) => {
|
|
38
|
+
const { name = '' } = req.params;
|
|
39
|
+
if (isItem(req.body)) {
|
|
40
|
+
res.locals['data'] = await service.create(name, req.body);
|
|
41
|
+
}
|
|
42
|
+
next();
|
|
43
|
+
});
|
|
44
|
+
app.put('/:name/:id', async (req, res, next) => {
|
|
45
|
+
const { name = '', id = '' } = req.params;
|
|
46
|
+
if (isItem(req.body)) {
|
|
47
|
+
res.locals['data'] = await service.update(name, id, req.body);
|
|
48
|
+
}
|
|
49
|
+
next();
|
|
50
|
+
});
|
|
51
|
+
app.patch('/:name/:id', async (req, res, next) => {
|
|
52
|
+
const { name = '', id = '' } = req.params;
|
|
53
|
+
if (isItem(req.body)) {
|
|
54
|
+
res.locals['data'] = await service.patch(name, id, req.body);
|
|
55
|
+
}
|
|
56
|
+
next();
|
|
57
|
+
});
|
|
58
|
+
app.delete('/:name/:id', async (req, res, next) => {
|
|
59
|
+
const { name = '', id = '' } = req.params;
|
|
60
|
+
res.locals['data'] = await service.destroy(name, id);
|
|
61
|
+
next();
|
|
62
|
+
});
|
|
63
|
+
app.use('/:name', (req, res) => {
|
|
64
|
+
const { data } = res.locals;
|
|
65
|
+
if (data === undefined) {
|
|
66
|
+
res.sendStatus(404);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
if (req.method === 'POST')
|
|
70
|
+
res.status(201);
|
|
71
|
+
res.json(data);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
return app;
|
|
75
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/app.test.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
import getPort from 'get-port';
|
|
6
|
+
import { Low, Memory } from 'lowdb';
|
|
7
|
+
import { temporaryDirectory } from 'tempy';
|
|
8
|
+
import { createApp } from './app.js';
|
|
9
|
+
const port = await getPort();
|
|
10
|
+
// Create custom static dir with an html file
|
|
11
|
+
const tmpDir = temporaryDirectory();
|
|
12
|
+
const file = 'file.html';
|
|
13
|
+
writeFileSync(join(tmpDir, file), 'utf-8');
|
|
14
|
+
// Create app
|
|
15
|
+
const db = new Low(new Memory(), {});
|
|
16
|
+
db.data = {
|
|
17
|
+
posts: [{ id: '1', title: 'foo' }],
|
|
18
|
+
comments: [{ id: '1', postId: '1' }],
|
|
19
|
+
};
|
|
20
|
+
const app = createApp(db, { static: [tmpDir] });
|
|
21
|
+
await new Promise((resolve, reject) => {
|
|
22
|
+
try {
|
|
23
|
+
const server = app.listen(port, () => resolve());
|
|
24
|
+
test.after(() => server.close());
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
reject(err);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
await test('createApp', async () => {
|
|
31
|
+
// URLs
|
|
32
|
+
const POSTS = '/posts';
|
|
33
|
+
const POST_1 = '/posts/1';
|
|
34
|
+
const POST_NOT_FOUND = '/posts/-1';
|
|
35
|
+
const COMMENTS = '/comments';
|
|
36
|
+
const POST_COMMENTS = '/comments?postId=1';
|
|
37
|
+
const NOT_FOUND = '/not-found';
|
|
38
|
+
const arr = [
|
|
39
|
+
// Static
|
|
40
|
+
{ method: 'GET', url: '/', statusCode: 200 },
|
|
41
|
+
{ method: 'GET', url: '/output.css', statusCode: 200 },
|
|
42
|
+
{ method: 'GET', url: `/${file}`, statusCode: 200 },
|
|
43
|
+
// API
|
|
44
|
+
{ method: 'GET', url: POSTS, statusCode: 200 },
|
|
45
|
+
{ method: 'GET', url: POST_1, statusCode: 200 },
|
|
46
|
+
{ method: 'GET', url: POST_NOT_FOUND, statusCode: 404 },
|
|
47
|
+
{ method: 'GET', url: COMMENTS, statusCode: 200 },
|
|
48
|
+
{ method: 'GET', url: POST_COMMENTS, statusCode: 200 },
|
|
49
|
+
{ method: 'GET', url: NOT_FOUND, statusCode: 404 },
|
|
50
|
+
{ method: 'POST', url: POSTS, statusCode: 201 },
|
|
51
|
+
{ method: 'POST', url: POST_1, statusCode: 404 },
|
|
52
|
+
{ method: 'POST', url: POST_NOT_FOUND, statusCode: 404 },
|
|
53
|
+
{ method: 'POST', url: NOT_FOUND, statusCode: 404 },
|
|
54
|
+
{ method: 'PUT', url: POSTS, statusCode: 404 },
|
|
55
|
+
{ method: 'PUT', url: POST_1, statusCode: 200 },
|
|
56
|
+
{ method: 'PUT', url: POST_NOT_FOUND, statusCode: 404 },
|
|
57
|
+
{ method: 'PUT', url: NOT_FOUND, statusCode: 404 },
|
|
58
|
+
{ method: 'PATCH', url: POSTS, statusCode: 404 },
|
|
59
|
+
{ method: 'PATCH', url: POST_1, statusCode: 200 },
|
|
60
|
+
{ method: 'PATCH', url: POST_NOT_FOUND, statusCode: 404 },
|
|
61
|
+
{ method: 'PATCH', url: NOT_FOUND, statusCode: 404 },
|
|
62
|
+
{ method: 'DELETE', url: POSTS, statusCode: 404 },
|
|
63
|
+
{ method: 'DELETE', url: POST_1, statusCode: 200 },
|
|
64
|
+
{ method: 'DELETE', url: POST_NOT_FOUND, statusCode: 404 },
|
|
65
|
+
{ method: 'DELETE', url: NOT_FOUND, statusCode: 404 },
|
|
66
|
+
];
|
|
67
|
+
for (const tc of arr) {
|
|
68
|
+
const response = await fetch(`http://localhost:${port}${tc.url}`, {
|
|
69
|
+
method: tc.method,
|
|
70
|
+
});
|
|
71
|
+
assert.equal(response.status, tc.statusCode, `${response.status} !== ${tc.statusCode} ${tc.method} ${tc.url} failed`);
|
|
72
|
+
}
|
|
73
|
+
});
|
package/lib/bin.d.ts
ADDED
package/lib/bin.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { extname, join } from 'node:path';
|
|
4
|
+
import { parseArgs } from 'node:util';
|
|
5
|
+
import { watch } from 'chokidar';
|
|
6
|
+
import { Low } from 'lowdb';
|
|
7
|
+
import { JSONFile } from 'lowdb/node';
|
|
8
|
+
import { createApp } from './app.js';
|
|
9
|
+
import { JSON5File } from './JSON5File.js';
|
|
10
|
+
import { Observer } from './Observer.js';
|
|
11
|
+
// Parse args
|
|
12
|
+
const { values, positionals } = parseArgs({
|
|
13
|
+
args: process.argv.slice(2),
|
|
14
|
+
options: {
|
|
15
|
+
port: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
short: 'p',
|
|
18
|
+
},
|
|
19
|
+
host: {
|
|
20
|
+
type: 'string',
|
|
21
|
+
short: 'h',
|
|
22
|
+
},
|
|
23
|
+
static: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
short: 's',
|
|
26
|
+
multiple: true,
|
|
27
|
+
},
|
|
28
|
+
help: {
|
|
29
|
+
type: 'boolean',
|
|
30
|
+
},
|
|
31
|
+
version: {
|
|
32
|
+
type: 'boolean',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
allowPositionals: true,
|
|
36
|
+
});
|
|
37
|
+
if (values.help || positionals.length === 0) {
|
|
38
|
+
console.log(`Usage: json-server [options] [file]
|
|
39
|
+
Options:
|
|
40
|
+
-p, --port <port> Port (default: 3000)
|
|
41
|
+
-h, --host <host> Host (default: localhost)
|
|
42
|
+
-s, --static <dir> Static files directory (multiple allowed)
|
|
43
|
+
--help Show this message
|
|
44
|
+
`);
|
|
45
|
+
}
|
|
46
|
+
if (values.version) {
|
|
47
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'));
|
|
48
|
+
console.log(pkg.version);
|
|
49
|
+
process.exit();
|
|
50
|
+
}
|
|
51
|
+
// App args and options
|
|
52
|
+
const file = positionals[0] ?? 'db.json';
|
|
53
|
+
const port = parseInt(values.port ?? process.env['PORT'] ?? '3000');
|
|
54
|
+
const host = values.host ?? process.env['HOST'] ?? 'localhost';
|
|
55
|
+
// Set up database
|
|
56
|
+
let adapter;
|
|
57
|
+
if (extname(file) === '.json5') {
|
|
58
|
+
adapter = new JSON5File(file);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
adapter = new JSONFile(file);
|
|
62
|
+
}
|
|
63
|
+
const observer = new Observer(adapter);
|
|
64
|
+
const db = new Low(observer, {});
|
|
65
|
+
await db.read();
|
|
66
|
+
// Create app
|
|
67
|
+
const app = createApp(db, { logger: false, static: values.static });
|
|
68
|
+
function routes(db) {
|
|
69
|
+
return Object.keys(db.data).map((key) => `http://${host}:${port}/${key}`);
|
|
70
|
+
}
|
|
71
|
+
// Watch file for changes
|
|
72
|
+
if (process.env['NODE_ENV'] !== 'production') {
|
|
73
|
+
let writing = false; // true if the file is being written to by the app
|
|
74
|
+
observer.onWriteStart = () => {
|
|
75
|
+
writing = true;
|
|
76
|
+
};
|
|
77
|
+
observer.onWriteEnd = () => {
|
|
78
|
+
writing = false;
|
|
79
|
+
};
|
|
80
|
+
observer.onReadStart = () => console.log(`reloading ${file}...`);
|
|
81
|
+
observer.onReadEnd = () => console.log('reloaded');
|
|
82
|
+
watch(file).on('change', () => {
|
|
83
|
+
// Do no reload if the file is being written to by the app
|
|
84
|
+
if (!writing) {
|
|
85
|
+
db.read()
|
|
86
|
+
.then(() => routes(db))
|
|
87
|
+
.catch((e) => {
|
|
88
|
+
if (e instanceof SyntaxError) {
|
|
89
|
+
return console.log(e.message);
|
|
90
|
+
}
|
|
91
|
+
console.log(e);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
app.listen(port, () => {
|
|
97
|
+
console.log(`Started on :${port}`);
|
|
98
|
+
console.log(routes(db));
|
|
99
|
+
});
|
package/lib/service.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Low } from 'lowdb';
|
|
2
|
+
export type Headers = Record<string, string>;
|
|
3
|
+
export type Item = Record<string, unknown>;
|
|
4
|
+
export type Data = Record<string, Item[]>;
|
|
5
|
+
export declare function isItem(obj: unknown): obj is Item;
|
|
6
|
+
export declare function isData(obj: unknown): obj is Record<string, Item[]>;
|
|
7
|
+
export type PaginatedItems = {
|
|
8
|
+
first: number;
|
|
9
|
+
prev: number | null;
|
|
10
|
+
next: number | null;
|
|
11
|
+
last: number;
|
|
12
|
+
pages: number;
|
|
13
|
+
items: number;
|
|
14
|
+
data: Item[];
|
|
15
|
+
};
|
|
16
|
+
export declare class Service {
|
|
17
|
+
#private;
|
|
18
|
+
constructor(db: Low<Data>);
|
|
19
|
+
list(): string[];
|
|
20
|
+
has(name: string): boolean;
|
|
21
|
+
findById(name: string, id: string, query: {
|
|
22
|
+
_include?: string[];
|
|
23
|
+
}): Item | undefined;
|
|
24
|
+
find(name: string, query?: {
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
} & {
|
|
27
|
+
_include?: string[];
|
|
28
|
+
_sort?: string;
|
|
29
|
+
_start?: number;
|
|
30
|
+
_end?: number;
|
|
31
|
+
_limit?: number;
|
|
32
|
+
_page?: number;
|
|
33
|
+
_per_page?: number;
|
|
34
|
+
}): Item[] | PaginatedItems | undefined;
|
|
35
|
+
create(name: string, data?: Omit<Item, 'id'>): Promise<Item | undefined>;
|
|
36
|
+
update(name: string, id: string, body?: Omit<Item, 'id'>): Promise<Item | undefined>;
|
|
37
|
+
patch(name: string, id: string, body?: Omit<Item, 'id'>): Promise<Item | undefined>;
|
|
38
|
+
destroy(name: string, id: string, dependents?: string[]): Promise<Item | undefined>;
|
|
39
|
+
}
|
package/lib/service.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { getProperty } from 'dot-prop';
|
|
3
|
+
import inflection from 'inflection';
|
|
4
|
+
import sortOn from 'sort-on';
|
|
5
|
+
export function isItem(obj) {
|
|
6
|
+
return typeof obj === 'object' && obj !== null;
|
|
7
|
+
}
|
|
8
|
+
export function isData(obj) {
|
|
9
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
const data = obj;
|
|
13
|
+
return Object.values(data).every((value) => Array.isArray(value) && value.every(isItem));
|
|
14
|
+
}
|
|
15
|
+
var Operator;
|
|
16
|
+
(function (Operator) {
|
|
17
|
+
Operator["lt"] = "lt";
|
|
18
|
+
Operator["lte"] = "lte";
|
|
19
|
+
Operator["gt"] = "gt";
|
|
20
|
+
Operator["gte"] = "gte";
|
|
21
|
+
Operator["ne"] = "ne";
|
|
22
|
+
Operator["default"] = "";
|
|
23
|
+
})(Operator || (Operator = {}));
|
|
24
|
+
function isOperator(value) {
|
|
25
|
+
return Object.values(Operator).includes(value);
|
|
26
|
+
}
|
|
27
|
+
function include(db, name, item, related) {
|
|
28
|
+
if (inflection.singularize(related) === related) {
|
|
29
|
+
const relatedData = db.data[inflection.pluralize(related)];
|
|
30
|
+
if (!relatedData) {
|
|
31
|
+
return item;
|
|
32
|
+
}
|
|
33
|
+
const foreignKey = `${related}Id`;
|
|
34
|
+
const relatedItem = relatedData.find((relatedItem) => {
|
|
35
|
+
return relatedItem['id'] === item[foreignKey];
|
|
36
|
+
});
|
|
37
|
+
return { ...item, [related]: relatedItem };
|
|
38
|
+
}
|
|
39
|
+
const relatedData = db.data[related];
|
|
40
|
+
if (!relatedData) {
|
|
41
|
+
return item;
|
|
42
|
+
}
|
|
43
|
+
const foreignKey = `${inflection.singularize(name)}Id`;
|
|
44
|
+
const relatedItems = relatedData.filter((relatedItem) => relatedItem[foreignKey] === item['id']);
|
|
45
|
+
return { ...item, [related]: relatedItems };
|
|
46
|
+
}
|
|
47
|
+
function nullifyForeignKey(db, name, id) {
|
|
48
|
+
const foreignKey = `${inflection.singularize(name)}Id`;
|
|
49
|
+
Object.entries(db.data).forEach(([key, items]) => {
|
|
50
|
+
// Skip
|
|
51
|
+
if (key === name)
|
|
52
|
+
return;
|
|
53
|
+
// Nullify
|
|
54
|
+
items.forEach((item) => {
|
|
55
|
+
if (item[foreignKey] === id) {
|
|
56
|
+
item[foreignKey] = null;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function deleteDependents(db, name, dependents) {
|
|
62
|
+
const foreignKey = `${inflection.singularize(name)}Id`;
|
|
63
|
+
Object.entries(db.data).forEach(([key, items]) => {
|
|
64
|
+
// Skip
|
|
65
|
+
if (key === name || !dependents.includes(key))
|
|
66
|
+
return;
|
|
67
|
+
// Delete if foreign key is null
|
|
68
|
+
db.data[key] = items.filter((item) => item[foreignKey] !== null);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
export class Service {
|
|
72
|
+
#db;
|
|
73
|
+
constructor(db) {
|
|
74
|
+
this.#db = db;
|
|
75
|
+
}
|
|
76
|
+
#get(name) {
|
|
77
|
+
return this.#db.data[name];
|
|
78
|
+
}
|
|
79
|
+
list() {
|
|
80
|
+
return Object.keys(this.#db?.data || {});
|
|
81
|
+
}
|
|
82
|
+
has(name) {
|
|
83
|
+
return Object.prototype.hasOwnProperty.call(this.#db?.data, name);
|
|
84
|
+
}
|
|
85
|
+
findById(name, id, query) {
|
|
86
|
+
let item = this.#get(name)?.find((item) => item['id'] === id);
|
|
87
|
+
query._include?.forEach((related) => {
|
|
88
|
+
if (item !== undefined)
|
|
89
|
+
item = include(this.#db, name, item, related);
|
|
90
|
+
});
|
|
91
|
+
return item;
|
|
92
|
+
}
|
|
93
|
+
find(name, query = {}) {
|
|
94
|
+
let items = this.#get(name);
|
|
95
|
+
// Not found
|
|
96
|
+
if (items === undefined)
|
|
97
|
+
return;
|
|
98
|
+
// Include
|
|
99
|
+
query._include?.forEach((related) => {
|
|
100
|
+
if (items !== undefined)
|
|
101
|
+
items = items.map((item) => include(this.#db, name, item, related));
|
|
102
|
+
});
|
|
103
|
+
// Return list if no query params
|
|
104
|
+
if (Object.keys(query).length === 0) {
|
|
105
|
+
return items;
|
|
106
|
+
}
|
|
107
|
+
// Convert query params to conditions
|
|
108
|
+
const conds = {};
|
|
109
|
+
for (const [key, value] of Object.entries(query)) {
|
|
110
|
+
if (value === undefined || typeof value !== 'string') {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const re = /_(lt|lte|gt|gte|ne)$/;
|
|
114
|
+
const reArr = re.exec(key);
|
|
115
|
+
const op = reArr?.at(1);
|
|
116
|
+
if (op && isOperator(op)) {
|
|
117
|
+
const field = key.replace(re, '');
|
|
118
|
+
conds[field] = [op, value];
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (['_sort', '_start', '_end', '_limit', '_page', '_per_page'].includes(key))
|
|
122
|
+
continue;
|
|
123
|
+
conds[key] = [Operator.default, value];
|
|
124
|
+
}
|
|
125
|
+
// Loop through conditions and filter items
|
|
126
|
+
const res = items.filter((item) => {
|
|
127
|
+
for (const [key, [op, paramValue]] of Object.entries(conds)) {
|
|
128
|
+
if (paramValue && !Array.isArray(paramValue)) {
|
|
129
|
+
const itemValue = getProperty(item, key);
|
|
130
|
+
switch (op) {
|
|
131
|
+
// item_gt=value
|
|
132
|
+
case Operator.gt: {
|
|
133
|
+
if (!(typeof itemValue === 'number' &&
|
|
134
|
+
itemValue > parseInt(paramValue))) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
// item_gte=value
|
|
140
|
+
case Operator.gte: {
|
|
141
|
+
if (!(typeof itemValue === 'number' &&
|
|
142
|
+
itemValue >= parseInt(paramValue))) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
// item_lt=value
|
|
148
|
+
case Operator.lt: {
|
|
149
|
+
if (!(typeof itemValue === 'number' &&
|
|
150
|
+
itemValue < parseInt(paramValue))) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
// item_lte=value
|
|
156
|
+
case Operator.lte: {
|
|
157
|
+
if (!(typeof itemValue === 'number' &&
|
|
158
|
+
itemValue <= parseInt(paramValue))) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
// item_ne=value
|
|
164
|
+
case Operator.ne: {
|
|
165
|
+
if (!(itemValue != paramValue))
|
|
166
|
+
return false;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
// item=value
|
|
170
|
+
case Operator.default: {
|
|
171
|
+
if (!(itemValue == paramValue))
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return true;
|
|
178
|
+
});
|
|
179
|
+
const sort = query._sort || '';
|
|
180
|
+
const sorted = sortOn(res, sort.split(','));
|
|
181
|
+
const start = query._start;
|
|
182
|
+
const end = query._end;
|
|
183
|
+
const limit = query._limit;
|
|
184
|
+
if (start === undefined && limit) {
|
|
185
|
+
return sorted.slice(0, limit);
|
|
186
|
+
}
|
|
187
|
+
if (start && limit) {
|
|
188
|
+
return sorted.slice(start, start + limit);
|
|
189
|
+
}
|
|
190
|
+
let page = query._page;
|
|
191
|
+
const perPage = query._per_page || 10;
|
|
192
|
+
if (page) {
|
|
193
|
+
const items = sorted.length;
|
|
194
|
+
const pages = Math.ceil(items / perPage);
|
|
195
|
+
// Ensure page is within the valid range
|
|
196
|
+
page = Math.max(1, Math.min(page, pages));
|
|
197
|
+
const first = 1;
|
|
198
|
+
const prev = page > 1 ? page - 1 : null;
|
|
199
|
+
const next = page < pages ? page + 1 : null;
|
|
200
|
+
const last = pages;
|
|
201
|
+
const start = (page - 1) * perPage;
|
|
202
|
+
const end = start + perPage;
|
|
203
|
+
const data = sorted.slice(start, end);
|
|
204
|
+
return {
|
|
205
|
+
first,
|
|
206
|
+
prev,
|
|
207
|
+
next,
|
|
208
|
+
last,
|
|
209
|
+
pages,
|
|
210
|
+
items,
|
|
211
|
+
data,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return sorted.slice(start, end);
|
|
215
|
+
}
|
|
216
|
+
async create(name, data = {}) {
|
|
217
|
+
const items = this.#get(name);
|
|
218
|
+
if (items === undefined)
|
|
219
|
+
return;
|
|
220
|
+
const nextData = { id: randomBytes(2).toString('hex'), ...data };
|
|
221
|
+
items.push(nextData);
|
|
222
|
+
await this.#db.write();
|
|
223
|
+
return nextData;
|
|
224
|
+
}
|
|
225
|
+
async update(name, id, body = {}) {
|
|
226
|
+
const items = this.#get(name);
|
|
227
|
+
if (items === undefined)
|
|
228
|
+
return;
|
|
229
|
+
const index = items.findIndex((item) => item['id'] === id);
|
|
230
|
+
if (index === -1)
|
|
231
|
+
return;
|
|
232
|
+
const item = items.at(index);
|
|
233
|
+
if (item) {
|
|
234
|
+
const nextItem = { ...body, id: item['id'] };
|
|
235
|
+
items.splice(index, 1, nextItem);
|
|
236
|
+
await this.#db.write();
|
|
237
|
+
return nextItem;
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
async patch(name, id, body = {}) {
|
|
242
|
+
const items = this.#get(name);
|
|
243
|
+
if (items === undefined)
|
|
244
|
+
return;
|
|
245
|
+
const index = items.findIndex((item) => item['id'] === id);
|
|
246
|
+
if (index === -1)
|
|
247
|
+
return;
|
|
248
|
+
const item = items.at(index);
|
|
249
|
+
if (item) {
|
|
250
|
+
const nextItem = { ...item, ...body, id: item['id'] };
|
|
251
|
+
items.splice(index, 1, nextItem);
|
|
252
|
+
await this.#db.write();
|
|
253
|
+
return nextItem;
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
async destroy(name, id, dependents = []) {
|
|
258
|
+
const items = this.#get(name);
|
|
259
|
+
if (items === undefined)
|
|
260
|
+
return;
|
|
261
|
+
const index = items.findIndex((item) => item['id'] === id);
|
|
262
|
+
if (index === -1)
|
|
263
|
+
return;
|
|
264
|
+
const item = items.splice(index, 1)[0];
|
|
265
|
+
nullifyForeignKey(this.#db, name, id);
|
|
266
|
+
deleteDependents(this.#db, name, dependents);
|
|
267
|
+
await this.#db.write();
|
|
268
|
+
return item;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|