@theshelf/database 0.3.2 → 0.4.0

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 CHANGED
@@ -1,9 +1,7 @@
1
1
 
2
- # Database | The Shelf
2
+ # Database core | The Shelf
3
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.
4
+ This package contains the definition of the CRUD operations. It uses a interchangeable driver system for performing the actual operations. An in-memory driver is included.
7
5
 
8
6
  ## Installation
9
7
 
@@ -11,42 +9,20 @@ This integration is based on simple CRUD operations and purposely does NOT suppo
11
9
  npm install @theshelf/database
12
10
  ```
13
11
 
14
- ## Drivers
15
-
16
- Currently, there are two drivers available:
17
-
18
- * **Memory** - non-persistent in memory storage (suited for testing).
19
- * **MongoDB** - persistent document storage.
20
-
21
12
  ## How to use
22
13
 
23
14
  The basic set up looks like this.
24
15
 
25
16
  ```ts
26
- import Database, { MemoryDriver | MongoDBDriver as SelectedDriver } from '@theshelf/database';
17
+ import Database, { MemoryDriver } from '@theshelf/database';
27
18
 
28
- const driver = new SelectedDriver(/* configuration */);
19
+ const driver = new MemoryDriver();
29
20
  const database = new Database(driver);
30
21
 
31
22
  // Perform operations with the database instance
32
23
  ```
33
24
 
34
- ### Configuration
35
-
36
- #### Memory driver
37
-
38
- No configuration options.
39
-
40
- #### MongoDB driver
41
-
42
- ```ts
43
- type MongoDBConfiguration = {
44
- connectionString: string; // e.g. "mongodb://development:development@localhost:27017"
45
- databaseName: string; // e.g. "mydb"
46
- };
47
- ```
48
-
49
- ### Operations
25
+ ## Operations
50
26
 
51
27
  ```ts
52
28
  import { RecordData, RecordQuery, RecordSort, SortDirections } from '@theshelf/database';
@@ -95,7 +71,7 @@ await database.updateRecord('items', item.id, { 'name': item.name });
95
71
  await database.deleteRecord('items', item.id);
96
72
  ```
97
73
 
98
- ### Query options
74
+ ## Query options
99
75
 
100
76
  A basic query has the following structure.
101
77
 
@@ -112,7 +88,7 @@ const andQuery: RecordQuery = { AND: [ query1, query2, ...] }
112
88
  const orQuery: RecordQuery = { OR: [ query1, query2, ...] }
113
89
  ```
114
90
 
115
- ### Sort options
91
+ ## Sort options
116
92
 
117
93
  A basic query has the following structure.
118
94
 
@@ -127,3 +103,16 @@ const sort: RecordSort = { fieldName1: SortDirections.ASCENDING, fieldName2: Sor
127
103
  ```
128
104
 
129
105
  The sort will be performed in the configured order.
106
+
107
+ ## Drivers
108
+
109
+ There is one driver included in this package. Other drivers are available in separate packages.
110
+
111
+ ### Memory
112
+
113
+ In memory database (suited for testing). It doesn't have any configuration options, but has an additional operation.
114
+
115
+ ```ts
116
+ // Clear the memory
117
+ driver.clear();
118
+ ```
@@ -1,8 +1,9 @@
1
+ import type Logger from '@theshelf/logging';
1
2
  import type { Driver } from './definitions/interfaces.js';
2
3
  import type { RecordData, RecordField, RecordId, RecordQuery, RecordSort, RecordType } from './definitions/types.js';
3
- export default class Database implements Driver {
4
+ export default class Database {
4
5
  #private;
5
- constructor(driver: Driver);
6
+ constructor(driver: Driver, logger?: Logger);
6
7
  get connected(): boolean;
7
8
  connect(): Promise<void>;
8
9
  disconnect(): Promise<void>;
package/dist/Database.js CHANGED
@@ -1,40 +1,126 @@
1
1
  import sanitize from './utilities/sanitize.js';
2
+ import NotConnected from './errors/NotConnected.js';
2
3
  export default class Database {
3
4
  #driver;
4
- constructor(driver) {
5
+ #logger;
6
+ #logPrefix;
7
+ constructor(driver, logger) {
5
8
  this.#driver = driver;
9
+ this.#logger = logger?.for(Database.name);
10
+ this.#logPrefix = `${this.#driver.name} ->`;
6
11
  }
7
12
  get connected() {
8
13
  return this.#driver.connected;
9
14
  }
10
- connect() {
11
- return this.#driver.connect();
15
+ async connect() {
16
+ if (this.connected === true) {
17
+ return;
18
+ }
19
+ this.#logger?.debug(this.#logPrefix, 'Connecting');
20
+ try {
21
+ await this.#driver.connect();
22
+ }
23
+ catch (error) {
24
+ this.#logger?.error(this.#logPrefix, 'Connect failed with error', error);
25
+ throw error;
26
+ }
12
27
  }
13
- disconnect() {
14
- return this.#driver.disconnect();
28
+ async disconnect() {
29
+ if (this.connected === false) {
30
+ return;
31
+ }
32
+ this.#logger?.debug(this.#logPrefix, 'Disconnecting');
33
+ try {
34
+ return await this.#driver.disconnect();
35
+ }
36
+ catch (error) {
37
+ this.#logger?.error(this.#logPrefix, 'Disconnect failed with error', error);
38
+ throw error;
39
+ }
15
40
  }
16
- createRecord(type, data) {
17
- const cleanData = sanitize(data);
18
- return this.#driver.createRecord(type, cleanData);
41
+ async createRecord(type, data) {
42
+ this.#logger?.debug(this.#logPrefix, 'Creating record for type', type);
43
+ try {
44
+ this.#validateConnection();
45
+ const cleanData = sanitize(data);
46
+ return await this.#driver.createRecord(type, cleanData);
47
+ }
48
+ catch (error) {
49
+ this.#logger?.error(this.#logPrefix, 'Create record for type', type, 'failed with error', error);
50
+ throw error;
51
+ }
19
52
  }
20
- readRecord(type, query, fields, sort) {
21
- return this.#driver.readRecord(type, query, fields, sort);
53
+ async readRecord(type, query, fields, sort) {
54
+ this.#logger?.debug(this.#logPrefix, 'Reading record for type', type);
55
+ try {
56
+ this.#validateConnection();
57
+ return await this.#driver.readRecord(type, query, fields, sort);
58
+ }
59
+ catch (error) {
60
+ this.#logger?.error(this.#logPrefix, 'Read record for type', type, 'failed with error', error);
61
+ throw error;
62
+ }
22
63
  }
23
- searchRecords(type, query, fields, sort, limit, offset) {
24
- return this.#driver.searchRecords(type, query, fields, sort, limit, offset);
64
+ async searchRecords(type, query, fields, sort, limit, offset) {
65
+ this.#logger?.debug(this.#logPrefix, 'Searching record for type', type);
66
+ try {
67
+ this.#validateConnection();
68
+ return await this.#driver.searchRecords(type, query, fields, sort, limit, offset);
69
+ }
70
+ catch (error) {
71
+ this.#logger?.error(this.#logPrefix, 'Search record for type', type, 'failed with error', error);
72
+ throw error;
73
+ }
25
74
  }
26
- updateRecord(type, query, data) {
27
- const cleanData = sanitize(data);
28
- return this.#driver.updateRecord(type, query, cleanData);
75
+ async updateRecord(type, query, data) {
76
+ this.#logger?.debug(this.#logPrefix, 'Updating record for type', type);
77
+ try {
78
+ this.#validateConnection();
79
+ const cleanData = sanitize(data);
80
+ return await this.#driver.updateRecord(type, query, cleanData);
81
+ }
82
+ catch (error) {
83
+ this.#logger?.error(this.#logPrefix, 'Update record for type', type, 'failed with error', error);
84
+ throw error;
85
+ }
29
86
  }
30
- updateRecords(type, query, data) {
31
- const cleanData = sanitize(data);
32
- return this.#driver.updateRecords(type, query, cleanData);
87
+ async updateRecords(type, query, data) {
88
+ this.#logger?.debug(this.#logPrefix, 'Updating records for type', type);
89
+ try {
90
+ this.#validateConnection();
91
+ const cleanData = sanitize(data);
92
+ return await this.#driver.updateRecords(type, query, cleanData);
93
+ }
94
+ catch (error) {
95
+ this.#logger?.error(this.#logPrefix, 'Update records for type', type, 'failed with error', error);
96
+ throw error;
97
+ }
33
98
  }
34
- deleteRecord(type, query) {
35
- return this.#driver.deleteRecord(type, query);
99
+ async deleteRecord(type, query) {
100
+ this.#logger?.debug(this.#logPrefix, 'Deleting record for type', type);
101
+ try {
102
+ this.#validateConnection();
103
+ return await this.#driver.deleteRecord(type, query);
104
+ }
105
+ catch (error) {
106
+ this.#logger?.error(this.#logPrefix, 'Delete record for type', type, 'failed with error', error);
107
+ throw error;
108
+ }
36
109
  }
37
- deleteRecords(type, query) {
38
- return this.#driver.deleteRecords(type, query);
110
+ async deleteRecords(type, query) {
111
+ this.#logger?.debug(this.#logPrefix, 'Deleting records for type', type);
112
+ try {
113
+ this.#validateConnection();
114
+ return await this.#driver.deleteRecords(type, query);
115
+ }
116
+ catch (error) {
117
+ this.#logger?.error(this.#logPrefix, 'Delete records for type', type, 'failed with error', error);
118
+ throw error;
119
+ }
120
+ }
121
+ #validateConnection() {
122
+ if (this.connected === false) {
123
+ throw new NotConnected();
124
+ }
39
125
  }
40
126
  }
@@ -1,5 +1,6 @@
1
1
  import type { RecordData, RecordField, RecordId, RecordQuery, RecordSort, RecordType } from './types.js';
2
2
  export interface Driver {
3
+ get name(): string;
3
4
  get connected(): boolean;
4
5
  connect(): Promise<void>;
5
6
  disconnect(): Promise<void>;
@@ -2,6 +2,7 @@ import type { Driver } from '../definitions/interfaces.js';
2
2
  import type { RecordData, RecordQuery, RecordSort } from '../definitions/types.js';
3
3
  export default class Memory implements Driver {
4
4
  #private;
5
+ get name(): string;
5
6
  get connected(): boolean;
6
7
  get memory(): Map<string, RecordData[]>;
7
8
  connect(): Promise<void>;
@@ -16,6 +16,7 @@ export default class Memory {
16
16
  #memory = new Map();
17
17
  #connected = false;
18
18
  #recordId = 0;
19
+ get name() { return Memory.name; }
19
20
  get connected() { return this.#connected; }
20
21
  get memory() {
21
22
  if (this.#connected === false) {
package/dist/index.d.ts CHANGED
@@ -1,8 +1,7 @@
1
- export * from './definitions/constants.js';
2
- export type * from './definitions/interfaces.js';
3
- export type * from './definitions/types.js';
1
+ export { ID, LogicalOperators, SortDirections, QueryOperators } from './definitions/constants.js';
2
+ export type { Driver } from './definitions/interfaces.js';
3
+ export type { RecordType, RecordId, RecordField, RecordValue, RecordData, QueryOperator, QueryExpression, QuerySingleExpressionStatement, QueryMultiExpressionStatement, QuerySingleStatement, QueryMultiStatement, QueryStatement, RecordQuery, RecordDirection, RecordSort } from './definitions/types.js';
4
4
  export { default as DatabaseError } from './errors/DatabaseError.js';
5
5
  export { default as NotConnected } from './errors/NotConnected.js';
6
6
  export { default as MemoryDriver } from './drivers/Memory.js';
7
- export { default as MongoDBDriver } from './drivers/MongoDB.js';
8
7
  export { default } from './Database.js';
package/dist/index.js CHANGED
@@ -1,6 +1,5 @@
1
- export * from './definitions/constants.js';
1
+ export { ID, LogicalOperators, SortDirections, QueryOperators } from './definitions/constants.js';
2
2
  export { default as DatabaseError } from './errors/DatabaseError.js';
3
3
  export { default as NotConnected } from './errors/NotConnected.js';
4
4
  export { default as MemoryDriver } from './drivers/Memory.js';
5
- export { default as MongoDBDriver } from './drivers/MongoDB.js';
6
5
  export { default } from './Database.js';
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@theshelf/database",
3
3
  "private": false,
4
- "version": "0.3.2",
4
+ "version": "0.4.0",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "url": "git+https://github.com/MaskingTechnology/theshelf.git"
8
8
  },
9
+ "license": "MIT",
9
10
  "scripts": {
10
11
  "build": "tsc",
11
12
  "clean": "rimraf dist",
@@ -19,10 +20,15 @@
19
20
  "README.md",
20
21
  "dist"
21
22
  ],
22
- "types": "dist/index.d.ts",
23
+ "types": "./dist/index.d.ts",
23
24
  "exports": "./dist/index.js",
24
25
  "dependencies": {
25
- "mongodb": "7.0.0",
26
26
  "sanitize-html": "2.17.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/sanitize-html": "2.16.0"
30
+ },
31
+ "peerDependencies": {
32
+ "@theshelf/logging": "^0.4.0"
27
33
  }
28
34
  }
@@ -1,21 +0,0 @@
1
- import type { Driver } from '../definitions/interfaces.js';
2
- import type { RecordData, RecordField, RecordId, RecordQuery, RecordSort, RecordType } from '../definitions/types.js';
3
- type MongoDBConfiguration = {
4
- connectionString: string;
5
- databaseName: string;
6
- };
7
- export default class MongoDB implements Driver {
8
- #private;
9
- constructor(configuration: MongoDBConfiguration);
10
- get connected(): boolean;
11
- connect(): Promise<void>;
12
- disconnect(): Promise<void>;
13
- createRecord(type: RecordType, data: RecordData): Promise<RecordId>;
14
- readRecord(type: RecordType, query: RecordQuery, fields?: RecordField[], sort?: RecordSort): Promise<RecordData | undefined>;
15
- searchRecords(type: RecordType, query: RecordQuery, fields?: RecordField[], sort?: RecordSort, limit?: number, offset?: number): Promise<RecordData[]>;
16
- updateRecord(type: RecordType, query: RecordQuery, data: RecordData): Promise<number>;
17
- updateRecords(type: RecordType, query: RecordQuery, data: RecordData): Promise<number>;
18
- deleteRecord(type: RecordType, query: RecordQuery): Promise<number>;
19
- deleteRecords(type: RecordType, query: RecordQuery): Promise<number>;
20
- }
21
- export {};
@@ -1,187 +0,0 @@
1
- /* eslint @typescript-eslint/no-explicit-any: "off" */
2
- import { MongoClient } from 'mongodb';
3
- import { ID, LogicalOperators, QueryOperators, SortDirections } from '../definitions/constants.js';
4
- import DatabaseError from '../errors/DatabaseError.js';
5
- import NotConnected from '../errors/NotConnected.js';
6
- const UNKNOWN_ERROR = 'Unknown error';
7
- const OPERATORS = {
8
- [QueryOperators.EQUALS]: '$eq',
9
- [QueryOperators.GREATER_THAN]: '$gt',
10
- [QueryOperators.GREATER_THAN_OR_EQUALS]: '$gte',
11
- [QueryOperators.IN]: '$in',
12
- [QueryOperators.LESS_THAN]: '$lt',
13
- [QueryOperators.LESS_THAN_OR_EQUALS]: '$lte',
14
- [QueryOperators.NOT_EQUALS]: '$ne',
15
- [QueryOperators.NOT_IN]: '$nin',
16
- [QueryOperators.CONTAINS]: '$regex',
17
- [QueryOperators.STARTS_WITH]: '$regex',
18
- [QueryOperators.ENDS_WITH]: '$regex'
19
- };
20
- const LOGICAL_OPERATORS = {
21
- [LogicalOperators.AND]: '$and',
22
- [LogicalOperators.OR]: '$or'
23
- };
24
- const MONGO_ID = '_id';
25
- export default class MongoDB {
26
- #connectionString;
27
- #databaseName;
28
- #client;
29
- #database;
30
- #connected = false;
31
- constructor(configuration) {
32
- this.#connectionString = configuration.connectionString;
33
- this.#databaseName = configuration.databaseName;
34
- }
35
- get connected() { return this.#connected; }
36
- async connect() {
37
- try {
38
- this.#client = await this.#createClient(this.#connectionString);
39
- this.#client.on('close', () => { this.#connected = false; });
40
- this.#client.on('serverHeartbeatSucceeded', () => { this.#connected = true; });
41
- this.#client.on('serverHeartbeatFailed', () => { this.#connected = false; });
42
- this.#database = this.#getDatabase(this.#databaseName);
43
- this.#connected = true;
44
- }
45
- catch (error) {
46
- const message = error instanceof Error ? error.message : UNKNOWN_ERROR;
47
- throw new DatabaseError('Database connection failed: ' + message);
48
- }
49
- }
50
- async disconnect() {
51
- if (this.#client === undefined) {
52
- throw new NotConnected();
53
- }
54
- try {
55
- await this.#client.close();
56
- this.#connected = false;
57
- this.#client = undefined;
58
- this.#database = undefined;
59
- }
60
- catch (error) {
61
- const message = error instanceof Error ? error.message : UNKNOWN_ERROR;
62
- throw new DatabaseError('Database disconnection failed: ' + message);
63
- }
64
- }
65
- async createRecord(type, data) {
66
- const collection = await this.#getCollection(type);
67
- const dataCopy = { ...data };
68
- const id = dataCopy.id;
69
- delete dataCopy.id;
70
- await collection.insertOne({ _id: id, ...dataCopy });
71
- return id;
72
- }
73
- async readRecord(type, query, fields, sort) {
74
- const result = await this.searchRecords(type, query, fields, sort, 1, 0);
75
- return result[0];
76
- }
77
- async searchRecords(type, query, fields, sort, limit, offset) {
78
- const mongoQuery = this.#buildMongoQuery(query);
79
- const mongoSort = this.#buildMongoSort(sort);
80
- const collection = await this.#getCollection(type);
81
- const cursor = collection.find(mongoQuery, { sort: mongoSort, limit: limit, skip: offset });
82
- const result = await cursor.toArray();
83
- return result.map(data => this.#buildRecordData(data, fields));
84
- }
85
- async updateRecord(type, query, data) {
86
- const mongoQuery = this.#buildMongoQuery(query);
87
- const collection = await this.#getCollection(type);
88
- const result = await collection.updateOne(mongoQuery, { $set: data });
89
- return result.modifiedCount;
90
- }
91
- async updateRecords(type, query, data) {
92
- const mongoQuery = this.#buildMongoQuery(query);
93
- const collection = await this.#getCollection(type);
94
- const result = await collection.updateMany(mongoQuery, { $set: data });
95
- return result.modifiedCount;
96
- }
97
- async deleteRecord(type, query) {
98
- const mongoQuery = this.#buildMongoQuery(query);
99
- const collection = await this.#getCollection(type);
100
- const result = await collection.deleteOne(mongoQuery);
101
- return result.deletedCount;
102
- }
103
- async deleteRecords(type, query) {
104
- const mongoQuery = this.#buildMongoQuery(query);
105
- const collection = await this.#getCollection(type);
106
- const result = await collection.deleteMany(mongoQuery);
107
- return result.deletedCount;
108
- }
109
- #buildMongoQuery(query) {
110
- const mongoQuery = {};
111
- const multiStatements = query;
112
- const singleStatements = query;
113
- for (const key in multiStatements) {
114
- if (key === 'AND' || key === 'OR') {
115
- const singleMultiStatements = multiStatements[key] ?? [];
116
- const multiMongoQuery = [];
117
- for (const statement of singleMultiStatements) {
118
- const mongoQuery = this.#buildMongoQuery(statement);
119
- multiMongoQuery.push(mongoQuery);
120
- }
121
- const mongoKey = LOGICAL_OPERATORS[key];
122
- mongoQuery[mongoKey] = multiMongoQuery;
123
- continue;
124
- }
125
- const expression = singleStatements[key];
126
- const mongoKey = key === ID ? MONGO_ID : key;
127
- const mongoExpression = {};
128
- for (const operator in expression) {
129
- const value = this.#extractValue(expression, operator);
130
- const mongoOperator = OPERATORS[operator];
131
- mongoExpression[mongoOperator] = value;
132
- }
133
- mongoQuery[mongoKey] = mongoExpression;
134
- }
135
- return mongoQuery;
136
- }
137
- #buildMongoSort(sort) {
138
- const mongoSort = {};
139
- if (sort === undefined) {
140
- return mongoSort;
141
- }
142
- for (const element in sort) {
143
- const direction = sort[element];
144
- mongoSort[element] = direction === SortDirections.DESCENDING ? -1 : 1;
145
- }
146
- return mongoSort;
147
- }
148
- async #getCollection(name) {
149
- if (this.#database === undefined) {
150
- throw new NotConnected();
151
- }
152
- return this.#database.collection(name);
153
- }
154
- #getDatabase(databaseName) {
155
- if (this.#client === undefined) {
156
- throw new NotConnected();
157
- }
158
- return this.#client.db(databaseName);
159
- }
160
- async #createClient(connectionString) {
161
- return MongoClient.connect(connectionString);
162
- }
163
- #buildRecordData(data, fields) {
164
- const result = {};
165
- if (fields === undefined) {
166
- const recordData = { ...data };
167
- fields = Object.keys(recordData);
168
- const idIndex = fields.indexOf(MONGO_ID);
169
- fields[idIndex] = ID;
170
- }
171
- for (const field of fields) {
172
- const value = field === ID
173
- ? data[MONGO_ID]
174
- : data[field];
175
- result[field] = value ?? undefined;
176
- }
177
- return result;
178
- }
179
- #extractValue(expression, operator) {
180
- const value = expression[operator];
181
- switch (operator) {
182
- case QueryOperators.STARTS_WITH: return '^' + value;
183
- case QueryOperators.ENDS_WITH: return value + '$';
184
- }
185
- return value;
186
- }
187
- }