@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.
Files changed (42) hide show
  1. package/README.md +126 -0
  2. package/dist/Database.d.ts +18 -0
  3. package/dist/Database.js +44 -0
  4. package/dist/definitions/constants.d.ts +22 -0
  5. package/dist/definitions/constants.js +25 -0
  6. package/dist/definitions/interfaces.d.ts +15 -0
  7. package/dist/definitions/interfaces.js +1 -0
  8. package/dist/definitions/types.d.ts +16 -0
  9. package/dist/definitions/types.js +1 -0
  10. package/dist/errors/DatabaseError.d.ts +2 -0
  11. package/dist/errors/DatabaseError.js +2 -0
  12. package/dist/errors/NotConnected.d.ts +4 -0
  13. package/dist/errors/NotConnected.js +6 -0
  14. package/dist/errors/RecordNotCreated.d.ts +4 -0
  15. package/dist/errors/RecordNotCreated.js +6 -0
  16. package/dist/errors/RecordNotDeleted.d.ts +4 -0
  17. package/dist/errors/RecordNotDeleted.js +6 -0
  18. package/dist/errors/RecordNotFound.d.ts +4 -0
  19. package/dist/errors/RecordNotFound.js +6 -0
  20. package/dist/errors/RecordNotUpdated.d.ts +4 -0
  21. package/dist/errors/RecordNotUpdated.js +6 -0
  22. package/dist/errors/RecordsNotDeleted.d.ts +4 -0
  23. package/dist/errors/RecordsNotDeleted.js +6 -0
  24. package/dist/errors/RecordsNotUpdated.d.ts +4 -0
  25. package/dist/errors/RecordsNotUpdated.js +6 -0
  26. package/dist/errors/UnknownImplementation.d.ts +4 -0
  27. package/dist/errors/UnknownImplementation.js +6 -0
  28. package/dist/implementation.d.ts +3 -0
  29. package/dist/implementation.js +14 -0
  30. package/dist/implementations/memory/Memory.d.ts +18 -0
  31. package/dist/implementations/memory/Memory.js +197 -0
  32. package/dist/implementations/memory/create.d.ts +2 -0
  33. package/dist/implementations/memory/create.js +4 -0
  34. package/dist/implementations/mongodb/MongoDb.d.ts +18 -0
  35. package/dist/implementations/mongodb/MongoDb.js +216 -0
  36. package/dist/implementations/mongodb/create.d.ts +2 -0
  37. package/dist/implementations/mongodb/create.js +6 -0
  38. package/dist/index.d.ts +12 -0
  39. package/dist/index.js +12 -0
  40. package/dist/utilities/sanitize.d.ts +1 -0
  41. package/dist/utilities/sanitize.js +17 -0
  42. 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
+ }
@@ -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,2 @@
1
+ export default class DatabaseError extends Error {
2
+ }
@@ -0,0 +1,2 @@
1
+ export default class DatabaseError extends Error {
2
+ }
@@ -0,0 +1,4 @@
1
+ import DatabaseError from './DatabaseError';
2
+ export default class NotConnected extends DatabaseError {
3
+ constructor(message?: string);
4
+ }
@@ -0,0 +1,6 @@
1
+ import DatabaseError from './DatabaseError';
2
+ export default class NotConnected extends DatabaseError {
3
+ constructor(message) {
4
+ super(message ?? 'Database not connected');
5
+ }
6
+ }
@@ -0,0 +1,4 @@
1
+ import DatabaseError from './DatabaseError';
2
+ export default class RecordNotCreated extends DatabaseError {
3
+ constructor(message?: string);
4
+ }
@@ -0,0 +1,6 @@
1
+ import DatabaseError from './DatabaseError';
2
+ export default class RecordNotCreated extends DatabaseError {
3
+ constructor(message) {
4
+ super(message ?? 'Record not created');
5
+ }
6
+ }
@@ -0,0 +1,4 @@
1
+ import DatabaseError from './DatabaseError';
2
+ export default class RecordNotDeleted extends DatabaseError {
3
+ constructor(message?: string);
4
+ }
@@ -0,0 +1,6 @@
1
+ import DatabaseError from './DatabaseError';
2
+ export default class RecordNotDeleted extends DatabaseError {
3
+ constructor(message) {
4
+ super(message ?? 'Record not deleted');
5
+ }
6
+ }
@@ -0,0 +1,4 @@
1
+ import DatabaseError from './DatabaseError';
2
+ export default class RecordNotFound extends DatabaseError {
3
+ constructor(message?: string);
4
+ }
@@ -0,0 +1,6 @@
1
+ import DatabaseError from './DatabaseError';
2
+ export default class RecordNotFound extends DatabaseError {
3
+ constructor(message) {
4
+ super(message ?? 'Record not found');
5
+ }
6
+ }
@@ -0,0 +1,4 @@
1
+ import DatabaseError from './DatabaseError';
2
+ export default class RecordNotUpdated extends DatabaseError {
3
+ constructor(message?: string);
4
+ }
@@ -0,0 +1,6 @@
1
+ import DatabaseError from './DatabaseError';
2
+ export default class RecordNotUpdated extends DatabaseError {
3
+ constructor(message) {
4
+ super(message ?? 'Record not updated');
5
+ }
6
+ }
@@ -0,0 +1,4 @@
1
+ import DatabaseError from './DatabaseError';
2
+ export default class RecordsNotDeleted extends DatabaseError {
3
+ constructor(message?: string);
4
+ }
@@ -0,0 +1,6 @@
1
+ import DatabaseError from './DatabaseError';
2
+ export default class RecordsNotDeleted extends DatabaseError {
3
+ constructor(message) {
4
+ super(message ?? 'Records not deleted');
5
+ }
6
+ }
@@ -0,0 +1,4 @@
1
+ import DatabaseError from './DatabaseError';
2
+ export default class RecordsNotUpdated extends DatabaseError {
3
+ constructor(message?: string);
4
+ }
@@ -0,0 +1,6 @@
1
+ import DatabaseError from './DatabaseError';
2
+ export default class RecordsNotUpdated extends DatabaseError {
3
+ constructor(message) {
4
+ super(message ?? 'Records not updated');
5
+ }
6
+ }
@@ -0,0 +1,4 @@
1
+ import DatabaseError from './DatabaseError';
2
+ export default class UnknownImplementation extends DatabaseError {
3
+ constructor(name: string);
4
+ }
@@ -0,0 +1,6 @@
1
+ import DatabaseError from './DatabaseError';
2
+ export default class UnknownImplementation extends DatabaseError {
3
+ constructor(name) {
4
+ super(`Unknown database implementation: ${name}`);
5
+ }
6
+ }
@@ -0,0 +1,3 @@
1
+ import type { Driver } from './definitions/interfaces';
2
+ declare const _default: Driver;
3
+ export default _default;
@@ -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,2 @@
1
+ import Memory from './Memory';
2
+ export default function create(): Memory;
@@ -0,0 +1,4 @@
1
+ import Memory from './Memory';
2
+ export default function create() {
3
+ return new Memory();
4
+ }
@@ -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,2 @@
1
+ import MongoDb from './MongoDb';
2
+ export default function create(): MongoDb;
@@ -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
+ }
@@ -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
+ }