@theshelf/database 0.0.1
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 +126 -0
- package/dist/Database.d.ts +18 -0
- package/dist/Database.js +44 -0
- package/dist/definitions/constants.d.ts +22 -0
- package/dist/definitions/constants.js +25 -0
- package/dist/definitions/interfaces.d.ts +15 -0
- package/dist/definitions/interfaces.js +1 -0
- package/dist/definitions/types.d.ts +16 -0
- package/dist/definitions/types.js +1 -0
- package/dist/errors/DatabaseError.d.ts +2 -0
- package/dist/errors/DatabaseError.js +2 -0
- package/dist/errors/NotConnected.d.ts +4 -0
- package/dist/errors/NotConnected.js +6 -0
- package/dist/errors/RecordNotCreated.d.ts +4 -0
- package/dist/errors/RecordNotCreated.js +6 -0
- package/dist/errors/RecordNotDeleted.d.ts +4 -0
- package/dist/errors/RecordNotDeleted.js +6 -0
- package/dist/errors/RecordNotFound.d.ts +4 -0
- package/dist/errors/RecordNotFound.js +6 -0
- package/dist/errors/RecordNotUpdated.d.ts +4 -0
- package/dist/errors/RecordNotUpdated.js +6 -0
- package/dist/errors/RecordsNotDeleted.d.ts +4 -0
- package/dist/errors/RecordsNotDeleted.js +6 -0
- package/dist/errors/RecordsNotUpdated.d.ts +4 -0
- package/dist/errors/RecordsNotUpdated.js +6 -0
- package/dist/errors/UnknownImplementation.d.ts +4 -0
- package/dist/errors/UnknownImplementation.js +6 -0
- package/dist/implementation.d.ts +3 -0
- package/dist/implementation.js +14 -0
- package/dist/implementations/memory/Memory.d.ts +18 -0
- package/dist/implementations/memory/Memory.js +197 -0
- package/dist/implementations/memory/create.d.ts +2 -0
- package/dist/implementations/memory/create.js +4 -0
- package/dist/implementations/mongodb/MongoDb.d.ts +18 -0
- package/dist/implementations/mongodb/MongoDb.js +216 -0
- package/dist/implementations/mongodb/create.d.ts +2 -0
- package/dist/implementations/mongodb/create.js +6 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/utilities/sanitize.d.ts +1 -0
- package/dist/utilities/sanitize.js +17 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
|
|
2
|
+
# Database | The Shelf
|
|
3
|
+
|
|
4
|
+
The database integration provides a universal interaction layer with an actual data storage solution.
|
|
5
|
+
|
|
6
|
+
This integration is based on simple CRUD operations and purposely does NOT support relational querying.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @theshelf/database
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Implementations
|
|
15
|
+
|
|
16
|
+
Currently, there are two implementations:
|
|
17
|
+
|
|
18
|
+
* **Memory** - non-persistent in memory storage (suited for testing).
|
|
19
|
+
* **MongoDB** - persistent document storage.
|
|
20
|
+
|
|
21
|
+
## Configuration
|
|
22
|
+
|
|
23
|
+
The used implementation needs to be configured in the `.env` file.
|
|
24
|
+
|
|
25
|
+
```env
|
|
26
|
+
DATABASE_IMPLEMENTATION="mongodb" # (memory | mongodb)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
In case of MongoDB, additional configuration is required.
|
|
30
|
+
|
|
31
|
+
```env
|
|
32
|
+
MONGODB_CONNECTION_STRING="mongodb://username:password@address:27017"
|
|
33
|
+
MONGODB_DATABASE_NAME="mydb"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## How to use
|
|
37
|
+
|
|
38
|
+
An instance of the configured implementation can be imported for performing database operations.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import database from '@theshelf/database';
|
|
42
|
+
|
|
43
|
+
// Perform operations with the database instance
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Operations
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import database, { RecordData, RecordQuery, RecordSort, SortDirections } from '@theshelf/database';
|
|
50
|
+
|
|
51
|
+
// Open connection
|
|
52
|
+
await database.connect();
|
|
53
|
+
|
|
54
|
+
// Close connection
|
|
55
|
+
await database.disconnect();
|
|
56
|
+
|
|
57
|
+
// INSERT INTO items (name, quantity) VALUES (?, ?)
|
|
58
|
+
const id: string = await database.createRecord('items', { name: 'Popcorn', quantity: 3 });
|
|
59
|
+
|
|
60
|
+
// SELECT * FROM items WHERE id = ?
|
|
61
|
+
// Throws `RecordNotFound` if not found
|
|
62
|
+
const record: RecordData = await database.readRecord('items', id);
|
|
63
|
+
|
|
64
|
+
// SELECT name FROM items WHERE id = ?
|
|
65
|
+
const record: RecordData = await database.readRecord('items', id, ['name']);
|
|
66
|
+
|
|
67
|
+
// SELECT * FROM items
|
|
68
|
+
const records: RecordData[] = await database.searchRecords('items', {});
|
|
69
|
+
|
|
70
|
+
// SELECT name FROM items
|
|
71
|
+
const records: RecordData[] = await database.searchRecords('items', {}, ['name']);
|
|
72
|
+
|
|
73
|
+
// SELECT * FROM items WHERE id = ? LIMIT 1 OFFSET 0
|
|
74
|
+
const records: RecordData | undefined = await database.findRecord('items', { id }, undefined, undefined, 1, 0);
|
|
75
|
+
|
|
76
|
+
// SELECT * FROM items WHERE name LIKE "%?%" ORDER BY name ASC LIMIT ? OFFSET ?
|
|
77
|
+
const query: RecordQuery = { name: { CONTAINS: name }};
|
|
78
|
+
const sort: RecordSort = { name: SortDirections.ASCENDING };
|
|
79
|
+
const records: RecordData[] = await database.searchRecords('items', query, undefined, sort, limit, offset);
|
|
80
|
+
|
|
81
|
+
// SELECT name FROM items WHERE name LIKE "?%" OR name LIKE "%?" ORDER BY name ASC, quantity DESC LIMIT ? OFFSET ?;
|
|
82
|
+
const query: RecordQuery = { OR: [ { name: { STARTS_WITH: name } }, { name: { ENDS_WITH: name } } ] };
|
|
83
|
+
const sort: RecordSort = { name: SortDirections.ASCENDING, quantity: SortDirections.DESCENDING };
|
|
84
|
+
const records: RecordData[] = await database.searchRecords('items', query, ['name'], sort, limit, offset);
|
|
85
|
+
|
|
86
|
+
// UPDATE items SET name = ? WHERE id = ?
|
|
87
|
+
// Throws `RecordNotFound` if not found
|
|
88
|
+
await database.updateRecord('items', item.id, { 'name': item.name });
|
|
89
|
+
|
|
90
|
+
// DELETE FROM items WHERE id = ?
|
|
91
|
+
// Throws `RecordNotFound` if not found
|
|
92
|
+
await database.deleteRecord('items', item.id);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Query options
|
|
96
|
+
|
|
97
|
+
A basic query has the following structure.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
const query: RecordQuery = { fieldName1: { OPERATOR: value }, fieldName2: { OPERATOR: value }, ... }
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The following operators are supported: `EQUALS`, `NOT_EQUALS`, `LESS_THAN`, `LESS_THAN_OR_EQUALS`, `GREATER_THAN`, `GREATER_THAN_OR_EQUALS`, `IN`, `NOT_IN`, `CONTAINS`, `STARTS_WITH`, `ENDS_WITH`
|
|
104
|
+
|
|
105
|
+
Multiple queries can be grouped using the logical operators: `AND`, `OR`.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
const andQuery: RecordQuery = { AND: [ query1, query2, ...] }
|
|
109
|
+
const orQuery: RecordQuery = { OR: [ query1, query2, ...] }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Sort options
|
|
113
|
+
|
|
114
|
+
A basic query has the following structure.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const sort: RecordSort = { fieldName1: DIRECTION, fieldName2: DIRECTION, ... };
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The following directions are supported: `ASCENDING`, `DESCENDING`. Both are defined in the `SortDirections` enum.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const sort: RecordSort = { fieldName1: SortDirections.ASCENDING, fieldName2: SortDirections.DESCENDING, ... };
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The sort will be performed in the configured order.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Driver } from './definitions/interfaces';
|
|
2
|
+
import type { RecordData, RecordField, RecordId, RecordQuery, RecordSort, RecordType } from './definitions/types';
|
|
3
|
+
export default class Database implements Driver {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(driver: Driver);
|
|
6
|
+
get connected(): boolean;
|
|
7
|
+
connect(): Promise<void>;
|
|
8
|
+
disconnect(): Promise<void>;
|
|
9
|
+
createRecord(type: RecordType, data: RecordData): Promise<RecordId>;
|
|
10
|
+
readRecord(type: RecordType, id: RecordId, fields?: RecordField[]): Promise<RecordData>;
|
|
11
|
+
findRecord(type: RecordType, query: RecordQuery, fields?: RecordField[], sort?: RecordSort): Promise<RecordData | undefined>;
|
|
12
|
+
searchRecords(type: RecordType, query: RecordQuery, fields?: RecordField[], sort?: RecordSort, limit?: number, offset?: number): Promise<RecordData[]>;
|
|
13
|
+
updateRecord(type: RecordType, id: RecordId, data: RecordData): Promise<void>;
|
|
14
|
+
updateRecords(type: RecordType, query: RecordQuery, data: RecordData): Promise<void>;
|
|
15
|
+
deleteRecord(type: RecordType, id: RecordId): Promise<void>;
|
|
16
|
+
deleteRecords(type: RecordType, query: RecordQuery): Promise<void>;
|
|
17
|
+
clear(): Promise<void>;
|
|
18
|
+
}
|
package/dist/Database.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import sanitize from './utilities/sanitize';
|
|
2
|
+
export default class Database {
|
|
3
|
+
#driver;
|
|
4
|
+
constructor(driver) {
|
|
5
|
+
this.#driver = driver;
|
|
6
|
+
}
|
|
7
|
+
get connected() { return this.#driver.connected; }
|
|
8
|
+
connect() {
|
|
9
|
+
return this.#driver.connect();
|
|
10
|
+
}
|
|
11
|
+
disconnect() {
|
|
12
|
+
return this.#driver.disconnect();
|
|
13
|
+
}
|
|
14
|
+
createRecord(type, data) {
|
|
15
|
+
const cleanData = sanitize(data);
|
|
16
|
+
return this.#driver.createRecord(type, cleanData);
|
|
17
|
+
}
|
|
18
|
+
readRecord(type, id, fields) {
|
|
19
|
+
return this.#driver.readRecord(type, id, fields);
|
|
20
|
+
}
|
|
21
|
+
findRecord(type, query, fields, sort) {
|
|
22
|
+
return this.#driver.findRecord(type, query, fields, sort);
|
|
23
|
+
}
|
|
24
|
+
searchRecords(type, query, fields, sort, limit, offset) {
|
|
25
|
+
return this.#driver.searchRecords(type, query, fields, sort, limit, offset);
|
|
26
|
+
}
|
|
27
|
+
updateRecord(type, id, data) {
|
|
28
|
+
const cleanData = sanitize(data);
|
|
29
|
+
return this.#driver.updateRecord(type, id, cleanData);
|
|
30
|
+
}
|
|
31
|
+
updateRecords(type, query, data) {
|
|
32
|
+
const cleanData = sanitize(data);
|
|
33
|
+
return this.#driver.updateRecords(type, query, cleanData);
|
|
34
|
+
}
|
|
35
|
+
deleteRecord(type, id) {
|
|
36
|
+
return this.#driver.deleteRecord(type, id);
|
|
37
|
+
}
|
|
38
|
+
deleteRecords(type, query) {
|
|
39
|
+
return this.#driver.deleteRecords(type, query);
|
|
40
|
+
}
|
|
41
|
+
clear() {
|
|
42
|
+
return this.#driver.clear();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const ID = "id";
|
|
2
|
+
export declare const LogicalOperators: {
|
|
3
|
+
AND: string;
|
|
4
|
+
OR: string;
|
|
5
|
+
};
|
|
6
|
+
export declare const SortDirections: {
|
|
7
|
+
ASCENDING: string;
|
|
8
|
+
DESCENDING: string;
|
|
9
|
+
};
|
|
10
|
+
export declare const QueryOperators: {
|
|
11
|
+
EQUALS: string;
|
|
12
|
+
NOT_EQUALS: string;
|
|
13
|
+
LESS_THAN: string;
|
|
14
|
+
LESS_THAN_OR_EQUALS: string;
|
|
15
|
+
GREATER_THAN: string;
|
|
16
|
+
GREATER_THAN_OR_EQUALS: string;
|
|
17
|
+
IN: string;
|
|
18
|
+
NOT_IN: string;
|
|
19
|
+
CONTAINS: string;
|
|
20
|
+
STARTS_WITH: string;
|
|
21
|
+
ENDS_WITH: string;
|
|
22
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const ID = 'id';
|
|
2
|
+
export const LogicalOperators = {
|
|
3
|
+
AND: 'AND',
|
|
4
|
+
OR: 'OR'
|
|
5
|
+
};
|
|
6
|
+
export const SortDirections = {
|
|
7
|
+
ASCENDING: 'ASCENDING',
|
|
8
|
+
DESCENDING: 'DESCENDING'
|
|
9
|
+
};
|
|
10
|
+
export const QueryOperators = {
|
|
11
|
+
EQUALS: 'EQUALS',
|
|
12
|
+
NOT_EQUALS: 'NOT_EQUALS',
|
|
13
|
+
LESS_THAN: 'LESS_THAN',
|
|
14
|
+
LESS_THAN_OR_EQUALS: 'LESS_THAN_OR_EQUALS',
|
|
15
|
+
GREATER_THAN: 'GREATER_THAN',
|
|
16
|
+
GREATER_THAN_OR_EQUALS: 'GREATER_THAN_OR_EQUALS',
|
|
17
|
+
IN: 'IN',
|
|
18
|
+
NOT_IN: 'NOT_IN',
|
|
19
|
+
CONTAINS: 'CONTAINS', // "%LIKE%"
|
|
20
|
+
STARTS_WITH: 'STARTS_WITH', // "LIKE%"
|
|
21
|
+
ENDS_WITH: 'ENDS_WITH' // "%LIKE"
|
|
22
|
+
};
|
|
23
|
+
Object.freeze(LogicalOperators);
|
|
24
|
+
Object.freeze(SortDirections);
|
|
25
|
+
Object.freeze(QueryOperators);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RecordData, RecordField, RecordId, RecordQuery, RecordSort, RecordType } from './types';
|
|
2
|
+
export interface Driver {
|
|
3
|
+
get connected(): boolean;
|
|
4
|
+
connect(): Promise<void>;
|
|
5
|
+
disconnect(): Promise<void>;
|
|
6
|
+
createRecord(type: RecordType, data: RecordData): Promise<RecordId>;
|
|
7
|
+
readRecord(type: RecordType, id: RecordId, fields?: RecordField[]): Promise<RecordData>;
|
|
8
|
+
findRecord(type: RecordType, query: RecordQuery, fields?: RecordField[], sort?: RecordSort): Promise<RecordData | undefined>;
|
|
9
|
+
searchRecords(type: RecordType, query: RecordQuery, fields?: RecordField[], sort?: RecordSort, limit?: number, offset?: number): Promise<RecordData[]>;
|
|
10
|
+
updateRecord(type: RecordType, id: RecordId, data: RecordData): Promise<void>;
|
|
11
|
+
updateRecords(type: RecordType, query: RecordQuery, data: RecordData): Promise<void>;
|
|
12
|
+
deleteRecord(type: RecordType, id: RecordId): Promise<void>;
|
|
13
|
+
deleteRecords(type: RecordType, query: RecordQuery): Promise<void>;
|
|
14
|
+
clear(): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { QueryOperators, SortDirections } from './constants';
|
|
2
|
+
export type RecordType = string;
|
|
3
|
+
export type RecordId = string;
|
|
4
|
+
export type RecordField = string;
|
|
5
|
+
export type RecordValue = unknown;
|
|
6
|
+
export type RecordData = Record<RecordField, RecordValue>;
|
|
7
|
+
export type QueryOperator = keyof typeof QueryOperators;
|
|
8
|
+
export type QueryExpression = Partial<Record<QueryOperator, RecordValue>>;
|
|
9
|
+
export type QuerySingleExpressionStatement = Record<RecordField, QueryExpression>;
|
|
10
|
+
export type QueryMultiExpressionStatement = Partial<Record<'AND' | 'OR', QuerySingleExpressionStatement[]>>;
|
|
11
|
+
export type QuerySingleStatement = QuerySingleExpressionStatement | QueryMultiExpressionStatement;
|
|
12
|
+
export type QueryMultiStatement = Partial<Record<'AND' | 'OR', QuerySingleStatement[]>>;
|
|
13
|
+
export type QueryStatement = QuerySingleStatement | QueryMultiStatement;
|
|
14
|
+
export type RecordQuery = QueryStatement;
|
|
15
|
+
export type RecordDirection = keyof typeof SortDirections;
|
|
16
|
+
export type RecordSort = Record<RecordField, string>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import UnknownImplementation from './errors/UnknownImplementation';
|
|
2
|
+
import createMemoryDb from './implementations/memory/create';
|
|
3
|
+
import createMongoDb from './implementations/mongodb/create';
|
|
4
|
+
const implementations = new Map([
|
|
5
|
+
['memory', createMemoryDb],
|
|
6
|
+
['mongodb', createMongoDb],
|
|
7
|
+
]);
|
|
8
|
+
const DEFAULT_DATABASE_IMPLEMENTATION = 'memory';
|
|
9
|
+
const implementationName = process.env.DATABASE_IMPLEMENTATION ?? DEFAULT_DATABASE_IMPLEMENTATION;
|
|
10
|
+
const creator = implementations.get(implementationName.toLowerCase());
|
|
11
|
+
if (creator === undefined) {
|
|
12
|
+
throw new UnknownImplementation(implementationName);
|
|
13
|
+
}
|
|
14
|
+
export default creator();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Driver } from '../../definitions/interfaces';
|
|
2
|
+
import type { QueryStatement, RecordData, RecordSort } from '../../definitions/types';
|
|
3
|
+
export default class Memory implements Driver {
|
|
4
|
+
#private;
|
|
5
|
+
recordId: number;
|
|
6
|
+
get connected(): boolean;
|
|
7
|
+
connect(): Promise<void>;
|
|
8
|
+
disconnect(): Promise<void>;
|
|
9
|
+
createRecord(type: string, data: RecordData): Promise<string>;
|
|
10
|
+
readRecord(type: string, id: string, fields?: string[]): Promise<RecordData>;
|
|
11
|
+
findRecord(type: string, query: QueryStatement, fields?: string[], sort?: RecordSort): Promise<RecordData | undefined>;
|
|
12
|
+
searchRecords(type: string, query: QueryStatement, fields?: string[], sort?: RecordSort, limit?: number, offset?: number): Promise<RecordData[]>;
|
|
13
|
+
updateRecord(type: string, id: string, data: RecordData): Promise<void>;
|
|
14
|
+
updateRecords(type: string, query: QueryStatement, data: RecordData): Promise<void>;
|
|
15
|
+
deleteRecord(type: string, id: string): Promise<void>;
|
|
16
|
+
deleteRecords(type: string, query: QueryStatement): Promise<void>;
|
|
17
|
+
clear(): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { LogicalOperators, QueryOperators, SortDirections } from '../../definitions/constants';
|
|
2
|
+
import NotConnected from '../../errors/NotConnected';
|
|
3
|
+
import RecordNotFound from '../../errors/RecordNotFound';
|
|
4
|
+
import RecordNotUpdated from '../../errors/RecordNotUpdated';
|
|
5
|
+
const OPERATORS = {
|
|
6
|
+
[QueryOperators.EQUALS]: '==',
|
|
7
|
+
[QueryOperators.GREATER_THAN]: '>',
|
|
8
|
+
[QueryOperators.GREATER_THAN_OR_EQUALS]: '>=',
|
|
9
|
+
[QueryOperators.LESS_THAN]: '<',
|
|
10
|
+
[QueryOperators.LESS_THAN_OR_EQUALS]: '<=',
|
|
11
|
+
[QueryOperators.NOT_EQUALS]: '!=',
|
|
12
|
+
};
|
|
13
|
+
const LOGICAL_OPERATORS = {
|
|
14
|
+
[LogicalOperators.AND]: '&&',
|
|
15
|
+
[LogicalOperators.OR]: '||'
|
|
16
|
+
};
|
|
17
|
+
export default class Memory {
|
|
18
|
+
#memory = new Map();
|
|
19
|
+
#connected = false;
|
|
20
|
+
recordId = 0;
|
|
21
|
+
get connected() { return this.#connected; }
|
|
22
|
+
async connect() {
|
|
23
|
+
this.#connected = true;
|
|
24
|
+
}
|
|
25
|
+
async disconnect() {
|
|
26
|
+
this.#connected = false;
|
|
27
|
+
}
|
|
28
|
+
async createRecord(type, data) {
|
|
29
|
+
const collection = this.#getCollection(type);
|
|
30
|
+
const record = data.id === undefined
|
|
31
|
+
? { id: this.#createId(), ...data }
|
|
32
|
+
: data;
|
|
33
|
+
collection.push(record);
|
|
34
|
+
return record.id;
|
|
35
|
+
}
|
|
36
|
+
async readRecord(type, id, fields) {
|
|
37
|
+
const record = this.#fetchRecord(type, id);
|
|
38
|
+
if (record === undefined) {
|
|
39
|
+
throw new RecordNotFound();
|
|
40
|
+
}
|
|
41
|
+
return this.#buildRecordData(record, fields);
|
|
42
|
+
}
|
|
43
|
+
async findRecord(type, query, fields, sort) {
|
|
44
|
+
const result = await this.searchRecords(type, query, fields, sort, 1, 0);
|
|
45
|
+
return result[0];
|
|
46
|
+
}
|
|
47
|
+
async searchRecords(type, query, fields, sort, limit, offset) {
|
|
48
|
+
const records = this.#fetchRecords(type, query);
|
|
49
|
+
const sortedRecords = this.#sortRecords(records, sort);
|
|
50
|
+
const limitedRecords = this.#limitNumberOfRecords(sortedRecords, offset, limit);
|
|
51
|
+
return limitedRecords.map(record => this.#buildRecordData(record, fields));
|
|
52
|
+
}
|
|
53
|
+
async updateRecord(type, id, data) {
|
|
54
|
+
const record = this.#fetchRecord(type, id);
|
|
55
|
+
if (record === undefined) {
|
|
56
|
+
throw new RecordNotUpdated();
|
|
57
|
+
}
|
|
58
|
+
this.#updateRecordData(record, data);
|
|
59
|
+
}
|
|
60
|
+
async updateRecords(type, query, data) {
|
|
61
|
+
const records = this.#fetchRecords(type, query);
|
|
62
|
+
records.forEach(record => this.#updateRecordData(record, data));
|
|
63
|
+
}
|
|
64
|
+
async deleteRecord(type, id) {
|
|
65
|
+
const collection = this.#getCollection(type);
|
|
66
|
+
const index = collection.findIndex(record => record.id === id);
|
|
67
|
+
if (index === -1) {
|
|
68
|
+
throw new RecordNotFound();
|
|
69
|
+
}
|
|
70
|
+
collection.splice(index, 1);
|
|
71
|
+
}
|
|
72
|
+
async deleteRecords(type, query) {
|
|
73
|
+
const collection = this.#getCollection(type);
|
|
74
|
+
const records = this.#fetchRecords(type, query);
|
|
75
|
+
const indexes = records
|
|
76
|
+
.map(fetchedRecord => collection.findIndex(collectionRecord => collectionRecord.id === fetchedRecord.id))
|
|
77
|
+
.sort((a, b) => b - a); // Reverse the order of indexes to delete from the end to the beginning
|
|
78
|
+
indexes.forEach(index => collection.splice(index, 1));
|
|
79
|
+
}
|
|
80
|
+
async clear() {
|
|
81
|
+
this.#memory.clear();
|
|
82
|
+
}
|
|
83
|
+
#fetchRecord(type, id) {
|
|
84
|
+
const collection = this.#getCollection(type);
|
|
85
|
+
return collection.find(object => object.id === id);
|
|
86
|
+
}
|
|
87
|
+
#fetchRecords(type, query) {
|
|
88
|
+
const collection = this.#getCollection(type);
|
|
89
|
+
const filterFunction = this.#buildFilterFunction(query);
|
|
90
|
+
return collection.filter(filterFunction);
|
|
91
|
+
}
|
|
92
|
+
#updateRecordData(record, data) {
|
|
93
|
+
for (const key of Object.keys(data)) {
|
|
94
|
+
record[key] = data[key];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
#limitNumberOfRecords(result, offset, limit) {
|
|
98
|
+
if (offset === undefined && limit === undefined) {
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
const first = offset ?? 0;
|
|
102
|
+
const last = limit === undefined ? undefined : first + limit;
|
|
103
|
+
return result.slice(first, last);
|
|
104
|
+
}
|
|
105
|
+
#sortRecords(result, sort) {
|
|
106
|
+
if (sort === undefined) {
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
return result.sort((a, b) => {
|
|
110
|
+
for (const key in sort) {
|
|
111
|
+
const order = sort[key];
|
|
112
|
+
const valueA = a[key];
|
|
113
|
+
const valueB = b[key];
|
|
114
|
+
if (valueA > valueB) {
|
|
115
|
+
return order === SortDirections.ASCENDING ? 1 : -1;
|
|
116
|
+
}
|
|
117
|
+
else if (valueA < valueB) {
|
|
118
|
+
return order === SortDirections.ASCENDING ? -1 : 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return 0;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
#buildFilterFunction(query) {
|
|
125
|
+
const statementCode = this.#buildStatementCode(query);
|
|
126
|
+
const functionCode = statementCode === '' ? 'true' : statementCode;
|
|
127
|
+
// eslint-disable-next-line sonarjs/code-eval
|
|
128
|
+
return new Function('record', `return ${functionCode}`);
|
|
129
|
+
}
|
|
130
|
+
#buildStatementCode(query) {
|
|
131
|
+
const multiStatements = query;
|
|
132
|
+
const singleStatements = query;
|
|
133
|
+
const statementCodes = [];
|
|
134
|
+
for (const key in multiStatements) {
|
|
135
|
+
const code = key === 'AND' || key === 'OR'
|
|
136
|
+
? this.#buildMultiStatementCode(key, multiStatements[key] ?? [])
|
|
137
|
+
: this.#buildExpressionCode(key, singleStatements[key]);
|
|
138
|
+
statementCodes.push(code);
|
|
139
|
+
}
|
|
140
|
+
return statementCodes.join(' && ');
|
|
141
|
+
}
|
|
142
|
+
#buildMultiStatementCode(operator, statements) {
|
|
143
|
+
const codeOperator = LOGICAL_OPERATORS[operator];
|
|
144
|
+
const statementCodes = [];
|
|
145
|
+
for (const statement of statements) {
|
|
146
|
+
const statementCode = this.#buildStatementCode(statement);
|
|
147
|
+
statementCodes.push(statementCode);
|
|
148
|
+
}
|
|
149
|
+
const code = statementCodes.join(` ${codeOperator} `);
|
|
150
|
+
return `(${code})`;
|
|
151
|
+
}
|
|
152
|
+
#buildExpressionCode(key, expression) {
|
|
153
|
+
const expressionCodes = [];
|
|
154
|
+
for (const operator in expression) {
|
|
155
|
+
const value = expression[operator];
|
|
156
|
+
const expressionCode = this.#buildOperatorCode(key, operator, value);
|
|
157
|
+
expressionCodes.push(expressionCode);
|
|
158
|
+
}
|
|
159
|
+
return `(${expressionCodes.join(' && ')})`;
|
|
160
|
+
}
|
|
161
|
+
#buildOperatorCode(key, operator, value) {
|
|
162
|
+
const codeValue = JSON.stringify(value);
|
|
163
|
+
switch (operator) {
|
|
164
|
+
case QueryOperators.STARTS_WITH: return `record.${key}.startsWith(${codeValue})`;
|
|
165
|
+
case QueryOperators.ENDS_WITH: return `record.${key}.endsWith(${codeValue})`;
|
|
166
|
+
case QueryOperators.CONTAINS: return `record.${key}.includes(${codeValue})`;
|
|
167
|
+
case QueryOperators.IN: return `${codeValue}.includes(record.${key})`;
|
|
168
|
+
case QueryOperators.NOT_IN: return `!${codeValue}.includes(record.${key})`;
|
|
169
|
+
}
|
|
170
|
+
const codeOperator = OPERATORS[operator];
|
|
171
|
+
return `record.${key} ${codeOperator} ${codeValue}`;
|
|
172
|
+
}
|
|
173
|
+
#createId() {
|
|
174
|
+
return (++this.recordId).toString().padStart(8, '0');
|
|
175
|
+
}
|
|
176
|
+
#getCollection(type) {
|
|
177
|
+
if (this.#memory === undefined) {
|
|
178
|
+
throw new NotConnected();
|
|
179
|
+
}
|
|
180
|
+
let collection = this.#memory.get(type);
|
|
181
|
+
if (collection === undefined) {
|
|
182
|
+
collection = [];
|
|
183
|
+
this.#memory.set(type, collection);
|
|
184
|
+
}
|
|
185
|
+
return collection;
|
|
186
|
+
}
|
|
187
|
+
#buildRecordData(data, fields) {
|
|
188
|
+
if (fields === undefined) {
|
|
189
|
+
return { ...data };
|
|
190
|
+
}
|
|
191
|
+
const result = {};
|
|
192
|
+
for (const field of fields) {
|
|
193
|
+
result[field] = data[field];
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Driver } from '../../definitions/interfaces';
|
|
2
|
+
import type { RecordData, RecordField, RecordId, RecordQuery, RecordSort, RecordType } from '../../definitions/types';
|
|
3
|
+
export default class MongoDB implements Driver {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(connectionString: string, databaseName: string);
|
|
6
|
+
get connected(): boolean;
|
|
7
|
+
connect(): Promise<void>;
|
|
8
|
+
disconnect(): Promise<void>;
|
|
9
|
+
createRecord(type: RecordType, data: RecordData): Promise<RecordId>;
|
|
10
|
+
readRecord(type: RecordType, id: RecordId, fields?: RecordField[]): Promise<RecordData>;
|
|
11
|
+
findRecord(type: RecordType, query: RecordQuery, fields?: RecordField[], sort?: RecordSort): Promise<RecordData | undefined>;
|
|
12
|
+
searchRecords(type: RecordType, query: RecordQuery, fields?: RecordField[], sort?: RecordSort, limit?: number, offset?: number): Promise<RecordData[]>;
|
|
13
|
+
updateRecord(type: RecordType, id: RecordId, data: RecordData): Promise<void>;
|
|
14
|
+
updateRecords(type: RecordType, query: RecordQuery, data: RecordData): Promise<void>;
|
|
15
|
+
deleteRecord(type: RecordType, id: RecordId): Promise<void>;
|
|
16
|
+
deleteRecords(type: RecordType, query: RecordQuery): Promise<void>;
|
|
17
|
+
clear(): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/* eslint @typescript-eslint/no-explicit-any: "off" */
|
|
2
|
+
import { MongoClient } from 'mongodb';
|
|
3
|
+
import { ID, LogicalOperators, QueryOperators, SortDirections } from '../../definitions/constants';
|
|
4
|
+
import DatabaseError from '../../errors/DatabaseError';
|
|
5
|
+
import NotConnected from '../../errors/NotConnected';
|
|
6
|
+
import RecordNotCreated from '../../errors/RecordNotCreated';
|
|
7
|
+
import RecordNotDeleted from '../../errors/RecordNotDeleted';
|
|
8
|
+
import RecordNotFound from '../../errors/RecordNotFound';
|
|
9
|
+
import RecordNotUpdated from '../../errors/RecordNotUpdated';
|
|
10
|
+
import RecordsNotDeleted from '../../errors/RecordsNotDeleted';
|
|
11
|
+
import RecordsNotUpdated from '../../errors/RecordsNotUpdated';
|
|
12
|
+
const UNKNOWN_ERROR = 'Unknown error';
|
|
13
|
+
const OPERATORS = {
|
|
14
|
+
[QueryOperators.EQUALS]: '$eq',
|
|
15
|
+
[QueryOperators.GREATER_THAN]: '$gt',
|
|
16
|
+
[QueryOperators.GREATER_THAN_OR_EQUALS]: '$gte',
|
|
17
|
+
[QueryOperators.IN]: '$in',
|
|
18
|
+
[QueryOperators.LESS_THAN]: '$lt',
|
|
19
|
+
[QueryOperators.LESS_THAN_OR_EQUALS]: '$lte',
|
|
20
|
+
[QueryOperators.NOT_EQUALS]: '$ne',
|
|
21
|
+
[QueryOperators.NOT_IN]: '$nin',
|
|
22
|
+
[QueryOperators.CONTAINS]: '$regex',
|
|
23
|
+
[QueryOperators.STARTS_WITH]: '$regex',
|
|
24
|
+
[QueryOperators.ENDS_WITH]: '$regex'
|
|
25
|
+
};
|
|
26
|
+
const LOGICAL_OPERATORS = {
|
|
27
|
+
[LogicalOperators.AND]: '$and',
|
|
28
|
+
[LogicalOperators.OR]: '$or'
|
|
29
|
+
};
|
|
30
|
+
const MONGO_ID = '_id';
|
|
31
|
+
export default class MongoDB {
|
|
32
|
+
#connectionString;
|
|
33
|
+
#databaseName;
|
|
34
|
+
#client;
|
|
35
|
+
#database;
|
|
36
|
+
#connected = false;
|
|
37
|
+
constructor(connectionString, databaseName) {
|
|
38
|
+
this.#connectionString = connectionString;
|
|
39
|
+
this.#databaseName = databaseName;
|
|
40
|
+
}
|
|
41
|
+
get connected() { return this.#connected; }
|
|
42
|
+
async connect() {
|
|
43
|
+
try {
|
|
44
|
+
this.#client = await this.#createClient(this.#connectionString);
|
|
45
|
+
this.#client.on('close', () => { this.#connected = false; });
|
|
46
|
+
this.#client.on('serverHeartbeatSucceeded', () => { this.#connected = true; });
|
|
47
|
+
this.#client.on('serverHeartbeatFailed', () => { this.#connected = false; });
|
|
48
|
+
this.#database = this.#getDatabase(this.#databaseName);
|
|
49
|
+
this.#connected = true;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
const message = error instanceof Error ? error.message : UNKNOWN_ERROR;
|
|
53
|
+
throw new DatabaseError('Database connection failed: ' + message);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async disconnect() {
|
|
57
|
+
if (this.#client === undefined) {
|
|
58
|
+
throw new NotConnected();
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
await this.#client.close();
|
|
62
|
+
this.#connected = false;
|
|
63
|
+
this.#client = undefined;
|
|
64
|
+
this.#database = undefined;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
const message = error instanceof Error ? error.message : UNKNOWN_ERROR;
|
|
68
|
+
throw new DatabaseError('Database disconnection failed: ' + message);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async createRecord(type, data) {
|
|
72
|
+
const collection = await this.#getCollection(type);
|
|
73
|
+
const dataCopy = { ...data };
|
|
74
|
+
const id = dataCopy.id;
|
|
75
|
+
delete dataCopy.id;
|
|
76
|
+
try {
|
|
77
|
+
await collection.insertOne({ _id: id, ...dataCopy });
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
const message = error instanceof Error ? error.message : UNKNOWN_ERROR;
|
|
81
|
+
throw new RecordNotCreated(message);
|
|
82
|
+
}
|
|
83
|
+
return id;
|
|
84
|
+
}
|
|
85
|
+
async readRecord(type, id, fields) {
|
|
86
|
+
const collection = await this.#getCollection(type);
|
|
87
|
+
const entry = await collection.findOne({ _id: id });
|
|
88
|
+
if (entry === null) {
|
|
89
|
+
throw new RecordNotFound(`Record ${type} found: ${id}`);
|
|
90
|
+
}
|
|
91
|
+
return this.#buildRecordData(entry, fields);
|
|
92
|
+
}
|
|
93
|
+
async findRecord(type, query, fields, sort) {
|
|
94
|
+
const result = await this.searchRecords(type, query, fields, sort, 1, 0);
|
|
95
|
+
return result[0];
|
|
96
|
+
}
|
|
97
|
+
async searchRecords(type, query, fields, sort, limit, offset) {
|
|
98
|
+
const mongoQuery = this.#buildMongoQuery(query);
|
|
99
|
+
const mongoSort = this.#buildMongoSort(sort);
|
|
100
|
+
const collection = await this.#getCollection(type);
|
|
101
|
+
const cursor = collection.find(mongoQuery, { sort: mongoSort, limit: limit, skip: offset });
|
|
102
|
+
const result = await cursor.toArray();
|
|
103
|
+
return result.map(data => this.#buildRecordData(data, fields));
|
|
104
|
+
}
|
|
105
|
+
async updateRecord(type, id, data) {
|
|
106
|
+
const collection = await this.#getCollection(type);
|
|
107
|
+
const entry = await collection.updateOne({ _id: id }, { $set: data });
|
|
108
|
+
if (entry.modifiedCount === 0) {
|
|
109
|
+
throw new RecordNotUpdated();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async updateRecords(type, query, data) {
|
|
113
|
+
const mongoQuery = this.#buildMongoQuery(query);
|
|
114
|
+
const collection = await this.#getCollection(type);
|
|
115
|
+
const result = await collection.updateMany(mongoQuery, { $set: data });
|
|
116
|
+
if (result.acknowledged === false) {
|
|
117
|
+
throw new RecordsNotUpdated();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async deleteRecord(type, id) {
|
|
121
|
+
const collection = await this.#getCollection(type);
|
|
122
|
+
const result = await collection.deleteOne({ _id: id });
|
|
123
|
+
if (result.deletedCount !== 1) {
|
|
124
|
+
throw new RecordNotDeleted();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async deleteRecords(type, query) {
|
|
128
|
+
const mongoQuery = this.#buildMongoQuery(query);
|
|
129
|
+
const collection = await this.#getCollection(type);
|
|
130
|
+
const result = await collection.deleteMany(mongoQuery);
|
|
131
|
+
if (result.acknowledged === false) {
|
|
132
|
+
throw new RecordsNotDeleted();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async clear() {
|
|
136
|
+
return; // Deliberately not implemented
|
|
137
|
+
}
|
|
138
|
+
#buildMongoQuery(query) {
|
|
139
|
+
const mongoQuery = {};
|
|
140
|
+
const multiStatements = query;
|
|
141
|
+
const singleStatements = query;
|
|
142
|
+
for (const key in multiStatements) {
|
|
143
|
+
if (key === 'AND' || key === 'OR') {
|
|
144
|
+
const singleMultiStatements = multiStatements[key] ?? [];
|
|
145
|
+
const multiMongoQuery = [];
|
|
146
|
+
for (const statement of singleMultiStatements) {
|
|
147
|
+
const mongoQuery = this.#buildMongoQuery(statement);
|
|
148
|
+
multiMongoQuery.push(mongoQuery);
|
|
149
|
+
}
|
|
150
|
+
const mongoKey = LOGICAL_OPERATORS[key];
|
|
151
|
+
mongoQuery[mongoKey] = multiMongoQuery;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const expression = singleStatements[key];
|
|
155
|
+
const mongoKey = key === ID ? MONGO_ID : key;
|
|
156
|
+
const mongoExpression = {};
|
|
157
|
+
for (const operator in expression) {
|
|
158
|
+
const value = this.#extractValue(expression, operator);
|
|
159
|
+
const mongoOperator = OPERATORS[operator];
|
|
160
|
+
mongoExpression[mongoOperator] = value;
|
|
161
|
+
}
|
|
162
|
+
mongoQuery[mongoKey] = mongoExpression;
|
|
163
|
+
}
|
|
164
|
+
return mongoQuery;
|
|
165
|
+
}
|
|
166
|
+
#buildMongoSort(sort) {
|
|
167
|
+
const mongoSort = {};
|
|
168
|
+
if (sort === undefined) {
|
|
169
|
+
return mongoSort;
|
|
170
|
+
}
|
|
171
|
+
for (const element in sort) {
|
|
172
|
+
const direction = sort[element];
|
|
173
|
+
mongoSort[element] = direction === SortDirections.DESCENDING ? -1 : 1;
|
|
174
|
+
}
|
|
175
|
+
return mongoSort;
|
|
176
|
+
}
|
|
177
|
+
async #getCollection(name) {
|
|
178
|
+
if (this.#database === undefined) {
|
|
179
|
+
throw new NotConnected();
|
|
180
|
+
}
|
|
181
|
+
return this.#database.collection(name);
|
|
182
|
+
}
|
|
183
|
+
#getDatabase(databaseName) {
|
|
184
|
+
if (this.#client === undefined) {
|
|
185
|
+
throw new NotConnected();
|
|
186
|
+
}
|
|
187
|
+
return this.#client.db(databaseName);
|
|
188
|
+
}
|
|
189
|
+
async #createClient(connectionString) {
|
|
190
|
+
return MongoClient.connect(connectionString);
|
|
191
|
+
}
|
|
192
|
+
#buildRecordData(data, fields) {
|
|
193
|
+
const result = {};
|
|
194
|
+
if (fields === undefined) {
|
|
195
|
+
const recordData = { ...data };
|
|
196
|
+
fields = Object.keys(recordData);
|
|
197
|
+
const idIndex = fields.indexOf(MONGO_ID);
|
|
198
|
+
fields[idIndex] = ID;
|
|
199
|
+
}
|
|
200
|
+
for (const field of fields) {
|
|
201
|
+
const value = field === ID
|
|
202
|
+
? data[MONGO_ID]
|
|
203
|
+
: data[field];
|
|
204
|
+
result[field] = value ?? undefined;
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
#extractValue(expression, operator) {
|
|
209
|
+
const value = expression[operator];
|
|
210
|
+
switch (operator) {
|
|
211
|
+
case QueryOperators.STARTS_WITH: return '^' + value;
|
|
212
|
+
case QueryOperators.ENDS_WITH: return value + '$';
|
|
213
|
+
}
|
|
214
|
+
return value;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import MongoDb from './MongoDb';
|
|
2
|
+
export default function create() {
|
|
3
|
+
const connectionString = process.env.MONGODB_CONNECTION_STRING ?? 'undefined';
|
|
4
|
+
const databaseName = process.env.MONGODB_DATABASE_NAME ?? 'undefined';
|
|
5
|
+
return new MongoDb(connectionString, databaseName);
|
|
6
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Database from './Database';
|
|
2
|
+
declare const database: Database;
|
|
3
|
+
export * from './definitions/constants';
|
|
4
|
+
export * from './definitions/types';
|
|
5
|
+
export { default as DatabaseError } from './errors/DatabaseError';
|
|
6
|
+
export { default as NotConnected } from './errors/NotConnected';
|
|
7
|
+
export { default as RecordNotCreated } from './errors/RecordNotCreated';
|
|
8
|
+
export { default as RecordNotDeleted } from './errors/RecordNotDeleted';
|
|
9
|
+
export { default as RecordNotFound } from './errors/RecordNotFound';
|
|
10
|
+
export { default as RecordNotUpdated } from './errors/RecordNotUpdated';
|
|
11
|
+
export type { Database };
|
|
12
|
+
export default database;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Database from './Database';
|
|
2
|
+
import implementation from './implementation';
|
|
3
|
+
const database = new Database(implementation);
|
|
4
|
+
export * from './definitions/constants';
|
|
5
|
+
export * from './definitions/types';
|
|
6
|
+
export { default as DatabaseError } from './errors/DatabaseError';
|
|
7
|
+
export { default as NotConnected } from './errors/NotConnected';
|
|
8
|
+
export { default as RecordNotCreated } from './errors/RecordNotCreated';
|
|
9
|
+
export { default as RecordNotDeleted } from './errors/RecordNotDeleted';
|
|
10
|
+
export { default as RecordNotFound } from './errors/RecordNotFound';
|
|
11
|
+
export { default as RecordNotUpdated } from './errors/RecordNotUpdated';
|
|
12
|
+
export default database;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function sanitize(input: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import sanitizeHtml from 'sanitize-html';
|
|
2
|
+
const options = {
|
|
3
|
+
allowedTags: [],
|
|
4
|
+
allowedAttributes: {}
|
|
5
|
+
};
|
|
6
|
+
function sanitizeValue(input) {
|
|
7
|
+
return typeof input === 'string'
|
|
8
|
+
? sanitizeHtml(input, options)
|
|
9
|
+
: input;
|
|
10
|
+
}
|
|
11
|
+
export default function sanitize(input) {
|
|
12
|
+
const result = {};
|
|
13
|
+
for (const [key, value] of Object.entries(input)) {
|
|
14
|
+
result[key] = sanitizeValue(value);
|
|
15
|
+
}
|
|
16
|
+
return result;
|
|
17
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@theshelf/database",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc",
|
|
8
|
+
"clean": "rimraf dist",
|
|
9
|
+
"test": "vitest run",
|
|
10
|
+
"test-coverage": "vitest run --coverage",
|
|
11
|
+
"lint": "eslint",
|
|
12
|
+
"review": "npm run build && npm run lint && npm run test",
|
|
13
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"README.md",
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"types": "dist/index.d.ts",
|
|
20
|
+
"exports": "./dist/index.js",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@theshelf/errors": "*",
|
|
23
|
+
"mongodb": "7.0.0",
|
|
24
|
+
"sanitize-html": "2.17.0"
|
|
25
|
+
}
|
|
26
|
+
}
|