sorla-db 1.1.0 → 1.2.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/index.d.ts ADDED
@@ -0,0 +1,89 @@
1
+ // Type declarations for sorla-db
2
+
3
+ export type Document = Record<string, unknown> & { _id?: unknown };
4
+
5
+ export type ComparisonOperators = {
6
+ $eq?: unknown;
7
+ $ne?: unknown;
8
+ $gt?: unknown;
9
+ $gte?: unknown;
10
+ $lt?: unknown;
11
+ $lte?: unknown;
12
+ $in?: unknown[];
13
+ $nin?: unknown[];
14
+ $not?: ComparisonOperators;
15
+ };
16
+
17
+ /** A query: field conditions (value or operators, "a.b" paths allowed) and nested $and / $or / $nor. */
18
+ export type Query = {
19
+ $and?: Query[];
20
+ $or?: Query[];
21
+ $nor?: Query[];
22
+ [field: string]: unknown;
23
+ };
24
+
25
+ export type Projection = Record<string, 0>;
26
+ export type SortSpec = Record<string, 1 | -1>;
27
+
28
+ export interface SortOptions {
29
+ /** Intl.Collator options plus the locale, e.g. { locale: 'pt-BR', sensitivity: 'base', numeric: true } */
30
+ collation?: Intl.CollatorOptions & { locale?: string };
31
+ }
32
+
33
+ export interface StoredDocument {
34
+ readonly _data: Document;
35
+ readonly [field: string]: unknown;
36
+ }
37
+
38
+ export declare class Cursor {
39
+ readonly length: number;
40
+ currIndex: number;
41
+ hasNext(): boolean;
42
+ next(): StoredDocument;
43
+ reset(): void;
44
+ /** Data of the first document, null when empty. */
45
+ first(): Document | null;
46
+ /** Data of the last document, null when empty. */
47
+ last(): Document | null;
48
+ /** Plain copies of the documents. */
49
+ toArray(): Document[];
50
+ sort(spec: SortSpec, options?: SortOptions): this;
51
+ projection(projection: Projection): void;
52
+ push(element: object): void;
53
+ }
54
+
55
+ export declare class Collection {
56
+ readonly first: Document;
57
+ readonly last: Document;
58
+ readonly all: StoredDocument[];
59
+ count(): number;
60
+ /** Inserts a copy of the document and returns its _id. */
61
+ insertOne(document: Document, callback?: (id: unknown) => void): unknown;
62
+ /** Validates every document first, then inserts them and returns their ids. */
63
+ insertMany(documents: Document[], callback?: (ids: unknown[]) => void): unknown[];
64
+ find(query: Query, projection?: Projection | null): Cursor;
65
+ /** Data of the first matching document, null when nothing matches. */
66
+ findOne(query: Query, projection?: Projection | null): Document | null;
67
+ }
68
+
69
+ export declare class Database {
70
+ readonly name: string;
71
+ createCollection(name: string, validationSchema?: unknown): void;
72
+ countCollections(): number;
73
+ [collection: string]: unknown;
74
+ }
75
+
76
+ declare class sorla {
77
+ constructor();
78
+ /** The selected database. Collections are properties of it: sorla.db.people */
79
+ readonly db: Database & Record<string, Collection>;
80
+ dbExists(name: string): boolean;
81
+ /** Position of the new database, or false when the name exists. */
82
+ createDb(name: string): number | false;
83
+ /** Selects a database, creating it when needed. */
84
+ useDb(name: string): void;
85
+ countDb(): number;
86
+ }
87
+
88
+ export { sorla };
89
+ export default sorla;
package/index.mjs ADDED
@@ -0,0 +1,5 @@
1
+ // ES module entry: import sorla from 'sorla-db'
2
+ import sorla from './src/sorla.js';
3
+
4
+ export { sorla };
5
+ export default sorla;
package/package.json CHANGED
@@ -1,11 +1,32 @@
1
1
  {
2
2
  "name": "sorla-db",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "A simple and temporary database focused to use in browser environment that will bring capabilities for other tools that will be launched as soon as possible.",
5
- "main": "dist/sorla.min.js",
5
+ "main": "src/sorla.js",
6
+ "module": "index.mjs",
7
+ "types": "index.d.ts",
8
+ "unpkg": "dist/sorla.min.js",
9
+ "jsdelivr": "dist/sorla.min.js",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./index.d.ts",
13
+ "import": "./index.mjs",
14
+ "require": "./src/sorla.js"
15
+ },
16
+ "./dist/sorla.min.js": "./dist/sorla.min.js",
17
+ "./src/*": "./src/*",
18
+ "./package.json": "./package.json"
19
+ },
20
+ "files": [
21
+ "src",
22
+ "!src/helper.js",
23
+ "dist",
24
+ "index.mjs",
25
+ "index.d.ts"
26
+ ],
6
27
  "scripts": {
28
+ "build": "node index.js",
7
29
  "mini": "node index.js",
8
- "build": "webpack",
9
30
  "test": "jest"
10
31
  },
11
32
  "repository": {
@@ -38,5 +59,8 @@
38
59
  "jsdom-global": "^3.0.2",
39
60
  "mocha": "^10.2.0",
40
61
  "terser": "^5.27.0"
62
+ },
63
+ "engines": {
64
+ "node": ">=18"
41
65
  }
42
66
  }
@@ -3,7 +3,6 @@
3
3
  * @description Cursor class is used to represent the cursor in the document, adding some extra capabilities to object.
4
4
  */
5
5
 
6
- const documents = require('../documents/document.js');
7
6
 
8
7
  class cursor {
9
8
  /**
@@ -45,7 +44,8 @@ class cursor {
45
44
  * @return array
46
45
  */
47
46
  toArray() {
48
- return structuredClone(this._data);
47
+ // the documents are proxies, which structuredClone cannot copy: clone their data
48
+ return this._data.map(doc => structuredClone(doc._data));
49
49
  }
50
50
 
51
51
  /**
@@ -63,7 +63,7 @@ class cursor {
63
63
  * @return mixed
64
64
  */
65
65
  first() {
66
- return this._data[0]._data;
66
+ return this._data.length ? this._data[0]._data : null;
67
67
  }
68
68
 
69
69
  /**
@@ -72,30 +72,45 @@ class cursor {
72
72
  * @return mixed
73
73
  */
74
74
  last() {
75
- return this._data[this._data.length - 1];
75
+ return this._data.length ? this._data[this._data.length - 1]._data : null;
76
76
  }
77
77
 
78
78
  /**
79
79
  * @method sort
80
- * @description Sort the cursor using collection
81
- * @param json sortQuery
82
- * @return void
80
+ * @description Sort the documents of the cursor, like MongoDB: {age: -1, name: 1}
81
+ * Values of different types follow MongoDB's order: missing and null, numbers, strings,
82
+ * objects, arrays, booleans, dates. Strings compare by code point unless a collation is given.
83
+ * @param json sortQuery field => 1 (ascending) or -1 (descending), "a.b" paths allowed
84
+ * @param json options {collation: {locale, sensitivity, numeric...}} (Intl.Collator options)
85
+ * @return cursor
83
86
  */
84
- sort(sortQuery) {
85
- const queryDoc = new documents(sortQuery);
87
+ sort(sortQuery, options = {}) {
88
+ if(sortQuery === null || typeof sortQuery !== 'object' || Array.isArray(sortQuery))
89
+ throw new Error('The sort query must be an object like {field: 1}');
90
+
91
+ const keys = Object.keys(sortQuery);
92
+ for(const key of keys)
93
+ {
94
+ if(sortQuery[key] !== 1 && sortQuery[key] !== -1)
95
+ throw new Error(`Sort direction of "${key}" must be 1 or -1`);
96
+ }
97
+
98
+ const collation = options.collation
99
+ ? new Intl.Collator(options.collation.locale, options.collation)
100
+ : null;
101
+
86
102
  this._data.sort((a, b) => {
87
- for(const key of Object.keys(sortQuery))
103
+ for(const key of keys)
88
104
  {
89
- const ascending = queryDoc[key] === 1;
90
- if(a[key] > b[key])
91
- return ascending ? 1 : -1;
92
-
93
- if(a[key] < b[key])
94
- return ascending ? -1 : 1;
105
+ const result = compareValues(a[key], b[key], collation);
106
+ if(result !== 0)
107
+ return result * sortQuery[key];
95
108
  }
96
109
 
97
110
  return 0;
98
111
  });
112
+
113
+ return this;
99
114
  }
100
115
 
101
116
  /**
@@ -141,4 +156,53 @@ class cursor {
141
156
  }
142
157
  }
143
158
 
159
+ /**
160
+ * Rank of a value in MongoDB's comparison order
161
+ * @param mixed value
162
+ * @return number
163
+ */
164
+ function typeRank(value) {
165
+ if(value === undefined || value === null) return 0;
166
+ if(typeof value === 'number') return 1;
167
+ if(typeof value === 'string') return 2;
168
+ // toString instead of instanceof: Dates from another realm (iframes, workers, vm) still count
169
+ if(Object.prototype.toString.call(value) === '[object Date]') return 6;
170
+ if(Array.isArray(value)) return 4;
171
+ if(typeof value === 'object') return 3;
172
+ if(typeof value === 'boolean') return 5;
173
+ return 7;
174
+ }
175
+
176
+ /**
177
+ * Compares two field values for sorting
178
+ * @param mixed a
179
+ * @param mixed b
180
+ * @param Intl.Collator|null collation
181
+ * @return number -1, 0 or 1
182
+ */
183
+ function compareValues(a, b, collation) {
184
+ const rankA = typeRank(a);
185
+ const rankB = typeRank(b);
186
+ if(rankA !== rankB) return rankA < rankB ? -1 : 1;
187
+
188
+ switch(rankA) {
189
+ case 0:
190
+ return 0;
191
+ case 2:
192
+ if(collation) return Math.sign(collation.compare(a, b));
193
+ break;
194
+ case 3:
195
+ case 4:
196
+ a = JSON.stringify(a);
197
+ b = JSON.stringify(b);
198
+ break;
199
+ case 6:
200
+ a = a.getTime();
201
+ b = b.getTime();
202
+ break;
203
+ }
204
+
205
+ return a < b ? -1 : a > b ? 1 : 0;
206
+ }
207
+
144
208
  module.exports = cursor;
@@ -1,7 +1,10 @@
1
1
  const utils = require('../utils');
2
- const cursor = require('./cursor');
3
- const manipulation = require('../query_language/manipulation');
4
2
 
3
+ /**
4
+ * A stored document. Fields read like a plain object, including "a.b" paths. Stored documents are
5
+ * read-only from the outside; find() returns copies (clone()) whose fields projection can remove.
6
+ * This module requires no other document or query module, which keeps the require graph acyclic.
7
+ */
5
8
  class documents
6
9
  {
7
10
  constructor(object)
@@ -9,24 +12,23 @@ class documents
9
12
  this._data = object;
10
13
  this._utils = new utils();
11
14
  this._cloned = false;
12
- this._deleteReceiver = null;
13
15
 
14
16
  return new Proxy(this, {
15
- deleteProperty: (target, prop, receiver) => {
16
- if(target._cloned && target._deleteReceiver instanceof cursor)
17
+ deleteProperty: (target, prop) => {
18
+ if(target._cloned && prop in target._data)
17
19
  {
18
- if(prop in target._data)
19
- {
20
- console.log('2', prop);
21
- delete target._data[prop];
22
- return true;
23
- }
24
-
25
- return false;
20
+ delete target._data[prop];
21
+ return true;
26
22
  }
23
+
24
+ return false;
27
25
  },
28
26
 
29
27
  get: (target, prop) => {
28
+ // symbols (Symbol.toPrimitive, Symbol.iterator...) belong to the wrapper, not to the data
29
+ if(typeof prop !== 'string')
30
+ return target[prop];
31
+
30
32
  if(prop.indexOf('.') > -1)
31
33
  return target._utils.getDotData(target._data, prop);
32
34
 
@@ -36,19 +38,15 @@ class documents
36
38
  return target[prop];
37
39
  },
38
40
 
39
- set: (target, prop, value, receiver) => {
40
- if(!(prop in target._data) && !target.hasOwnProperty(prop))
41
- return false;
42
-
43
- if(target.hasOwnProperty(prop) && ['_data', '_utils'].indexOf(prop) === -1){
44
- target[prop] = value;
41
+ set: (target, prop, value) => {
42
+ if(prop === '_cloned')
43
+ {
44
+ target._cloned = value;
45
45
  return true;
46
46
  }
47
47
 
48
- if(receiver instanceof manipulation || target._cloned && receiver instanceof cursor) {
49
- target._data[prop] = value;
50
- return true;
51
- }
48
+ if(typeof prop === 'string' && prop in target._data)
49
+ throw new Error(`Document fields are read-only ("${prop}"), change documents through the collection`);
52
50
 
53
51
  return false;
54
52
  }
@@ -69,18 +67,16 @@ class documents
69
67
 
70
68
  /**
71
69
  * @method delete prop
72
- * @description Delete a property from the document
70
+ * @description Delete a property from a copy returned by find() (used by projection)
73
71
  * @param string prop
74
- * @param cursor receiver
75
72
  * @return boolean
76
73
  */
77
- deleteProp(prop, receiver)
74
+ deleteProp(prop)
78
75
  {
79
- this._deleteReceiver = receiver;
80
- if(this._cloned)
81
- return Reflect.deleteProperty(this, prop);
76
+ if(!this._cloned)
77
+ return false;
82
78
 
83
- return false;
79
+ return Reflect.deleteProperty(this, prop);
84
80
  }
85
81
  }
86
82
 
@@ -52,106 +52,143 @@ class manipulation {
52
52
  }
53
53
 
54
54
  /**
55
- * Verify if exists some document that is not object
56
- * @param array documents
55
+ * Whether a value is a document: an object that is not null nor an array
56
+ * @param mixed value
57
57
  * @return boolean
58
58
  */
59
- _validateDocumentsType(documents)
59
+ _isDocument(value)
60
60
  {
61
- const count = documents.reduce((acc, doc) => {
62
- return typeof doc !== 'object' ? ++acc : acc;
63
- }, 0);
64
-
65
- return count;
61
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
66
62
  }
67
63
 
68
64
  /**
69
- * Verify if keys has prohibited characters
65
+ * Verify that no key, at any depth, has prohibited characters
70
66
  * @param object document
71
67
  * @return boolean
72
68
  */
73
69
  _validateKeys(document)
74
70
  {
75
- const keys = Object.keys(document);
76
71
  const operators = new operatorsCheck();
77
- for(let key of keys)
72
+ for(const key of Object.keys(document))
78
73
  {
79
- const hasProhibitedCharacters = this._prohibitedCharacters.reduce((acc, char) => {
80
- return key.indexOf(char) > -1 ? ++acc : acc;
81
- }, 0);
82
-
83
- if(hasProhibitedCharacters > 0 && !operators.isOperator(key))
74
+ const hasProhibitedCharacters = this._prohibitedCharacters.some(char => key.indexOf(char) > -1);
75
+ if(hasProhibitedCharacters && !operators.isOperator(key))
84
76
  return false;
85
77
 
86
- if(typeof document[key] === 'object')
87
- return this._validateKeys(document[key]);
88
-
89
- if(Array.isArray(document[key]))
78
+ const value = document[key];
79
+ const nested = Array.isArray(value) ? value : [value];
80
+ for(const item of nested)
90
81
  {
91
- for(let value of document[key])
92
- {
93
- if(typeof value === 'object')
94
- return this._validateKeys(value);
95
- }
82
+ // null is a valid value, only objects have keys to check
83
+ if(item !== null && typeof item === 'object' && !this._validateKeys(item))
84
+ return false;
96
85
  }
97
86
  }
98
87
 
99
88
  return true;
100
89
  }
101
90
 
91
+ /**
92
+ * Set of the _id values of the collection. It is rebuilt when its size no longer matches the
93
+ * documents, so removals done directly on the documents array are handled too.
94
+ * @return Set
95
+ */
96
+ _ids()
97
+ {
98
+ if(!this._idSet || this._idSet.size !== this._documents.length)
99
+ this._idSet = new Set(this._documents.map(doc => doc._data._id));
100
+
101
+ return this._idSet;
102
+ }
103
+
104
+ /**
105
+ * Copies a document, so the object given by the caller is never changed
106
+ * @param object document
107
+ * @return object
108
+ */
109
+ _copy(document)
110
+ {
111
+ try {
112
+ return structuredClone(document);
113
+ } catch(error) {
114
+ throw new Error('Documents can only contain cloneable values (no functions or DOM nodes)');
115
+ }
116
+ }
117
+
102
118
  /**
103
119
  * Inserts one document to the collection
104
120
  * @author Anderson Arruda < anderson@sysborg.com.br >
105
- * @version 1.0.0
121
+ * @version 1.2.0
106
122
  * @param object document
107
- * @param function callback - under construction
108
- * @return void
123
+ * @param function callback - receives the inserted _id
124
+ * @return mixed the _id of the inserted document
109
125
  **/
110
126
  insertOne(document, callback)
111
127
  {
112
- if(typeof document !== 'object' || Array.isArray(document))
128
+ if(!this._isDocument(document))
113
129
  throw new Error('Invalid document type');
114
130
 
115
131
  if(!this._validateKeys(document))
116
132
  throw new Error('Invalid document keys');
117
133
 
118
- if(typeof document._id === 'undefined')
134
+ const data = this._copy(document);
135
+ if(typeof data._id === 'undefined')
119
136
  {
120
- let _idKey = '';
121
137
  do {
122
- _idKey = this._generateKey();
123
- } while(this._documents.find(doc => doc._id === document._id));
124
-
125
- document._id = _idKey;
138
+ data._id = this._generateKey();
139
+ } while(this._ids().has(data._id));
140
+ }
141
+ else if(this._ids().has(data._id))
142
+ {
143
+ throw new Error(`Duplicate _id "${data._id}"`);
126
144
  }
127
145
 
128
- if(!this._validate(document))
146
+ if(!this._validate(data))
129
147
  throw new Error('Invalid document');
130
148
 
131
- this._documents.push(new documents(document));
149
+ this._documents.push(new documents(data));
150
+ this._ids().add(data._id);
132
151
 
133
152
  if(typeof callback === 'function')
134
- callback();
153
+ callback(data._id);
154
+
155
+ return data._id;
135
156
  }
136
157
 
137
158
  /**
138
- * Insert many documents
159
+ * Insert many documents. Every document is validated before the first one is inserted,
160
+ * so an invalid document never leaves the collection with only part of the batch.
139
161
  * @author Anderson Arruda < anderson@sysborg.com.br >
140
- * @version 1.0.0
162
+ * @version 1.2.0
141
163
  * @param array documents
142
- * @param function callback
143
- * @return void
164
+ * @param function callback - receives the inserted ids
165
+ * @return array the _id of each inserted document
144
166
  */
145
167
  insertMany(documents, callback) {
146
- console.info(this._validateDocumentsType(documents));
147
- if(!Array.isArray(documents) || this._validateDocumentsType(documents) > 0)
168
+ if(!Array.isArray(documents) || !documents.every(document => this._isDocument(document)))
148
169
  throw new Error('Documents must be an array of objects');
149
170
 
150
- for(let document of documents)
151
- this.insertOne(document);
171
+ const given = new Set();
172
+ for(const document of documents)
173
+ {
174
+ if(!this._validateKeys(document))
175
+ throw new Error('Invalid document keys');
176
+
177
+ if(typeof document._id === 'undefined')
178
+ continue;
179
+
180
+ if(given.has(document._id) || this._ids().has(document._id))
181
+ throw new Error(`Duplicate _id "${document._id}"`);
182
+
183
+ given.add(document._id);
184
+ }
185
+
186
+ const ids = documents.map(document => this.insertOne(document));
152
187
 
153
188
  if(typeof callback === 'function')
154
- callback();
189
+ callback(ids);
190
+
191
+ return ids;
155
192
  }
156
193
 
157
194
  //create index
package/src/utils.js CHANGED
@@ -25,16 +25,17 @@ class utils
25
25
  */
26
26
  getDotData(data, field)
27
27
  {
28
- let fields = field.split('.');
28
+ let value = data;
29
+ for(const part of field.split('.'))
30
+ {
31
+ // a missing step means the field does not exist (it used to return the parent object)
32
+ if(value === null || typeof value !== 'object' || !(part in value))
33
+ return undefined;
29
34
 
30
- fields.forEach(field => {
31
- if(typeof data[field] !== 'undefined')
32
- {
33
- data = data[field];
34
- }
35
- });
35
+ value = value[part];
36
+ }
36
37
 
37
- return data;
38
+ return value;
38
39
  }
39
40
  }
40
41
 
package/Brainstorming.md DELETED
@@ -1,22 +0,0 @@
1
- For the first query version needs only to see if has some non logical operator or non logical comparison to resend to find as a recursive method.
2
-
3
- - Preparing sort of data using new capabilities
4
- - improving find behavior
5
-
6
- For comparison operators and deny operator like not
7
- adding nor https://www.mongodb.com/docs/manual/reference/operator/query/nor/
8
-
9
- https://www.mongodb.com/docs/manual/reference/operator/query-comparison/
10
- https://www.mongodb.com/docs/manual/reference/operator/query/expr/
11
- https://www.mongodb.com/docs/manual/reference/operator/query/regex/
12
-
13
-
14
- Preparing a bunch of misc tests to search data, add sort data and create test sortable data.
15
-
16
- //erros and improvements
17
- Bugs appears after resolving not, need to check and test why
18
- npm run test
19
-
20
- needs to improve projection to use cursor
21
-
22
- fix new bugs in test
package/debug-not.js DELETED
@@ -1,24 +0,0 @@
1
- const sorla = require('./src/sorla.js');
2
- const helper = require('./src/helper.js');
3
-
4
- const numberOfObjects = 20;
5
- const sdb = new sorla();
6
- sdb.createDb('test');
7
- sdb.useDb('test');
8
-
9
- sdb.db.createCollection('collection_test');
10
-
11
- const objects = helper.createFakeCollection(sdb, 'collection_test', numberOfObjects);
12
- const [itemIndex1, ] = helper.getRandomComparisonData(objects);
13
-
14
- const finded = sdb.db.collection_test.find({
15
- uuid: {
16
- $not: {
17
- $eq: itemIndex1.uuid
18
- }
19
- }
20
- });
21
-
22
- console.log(finded.length, numberOfObjects);
23
-
24
- //console.log(finded);