sorla-db 1.1.0 → 1.3.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 +152 -9
- package/dist/sorla.min.js +1 -1
- package/index.d.ts +157 -0
- package/index.mjs +5 -0
- package/package.json +27 -3
- package/src/dbs/database.js +48 -0
- package/src/documents/cursor.js +141 -25
- package/src/documents/document.js +26 -30
- package/src/events.js +69 -0
- package/src/query_language/manipulation.js +321 -50
- package/src/query_language/operators.js +66 -1
- package/src/query_language/query.js +6 -2
- package/src/query_language/update.js +167 -0
- package/src/sorla.js +27 -0
- package/src/utils.js +9 -8
- package/Brainstorming.md +0 -22
- package/debug-not.js +0 -24
- package/example/simple_data_example.html +0 -196
- package/index.js +0 -62
- package/src/helper.js +0 -41
- package/test/feature_collection.test.js +0 -75
- package/test/feature_collection_queries.test.js +0 -224
- package/test/feature_cursor.test.js +0 -84
- package/test/feature_database.test.js +0 -62
- package/test/feature_insert.test.js +0 -116
- package/test/feature_nested_queries.test.js +0 -94
- package/test/feature_query_equivalence.test.js +0 -165
- package/test/unit_database.test.js +0 -36
- package/test/unit_document.test.js +0 -39
- package/test/unit_operators.test.js +0 -78
- package/test/unit_sorla.test.js +0 -42
- package/test/unit_test.test.js +0 -25
- package/webpack.config.js +0 -11
package/index.d.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
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
|
+
$exists?: boolean;
|
|
15
|
+
$regex?: string | RegExp;
|
|
16
|
+
/** Flags for $regex: i, m, s */
|
|
17
|
+
$options?: string;
|
|
18
|
+
$not?: ComparisonOperators | RegExp;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** A query: field conditions (value or operators, "a.b" paths allowed) and nested $and / $or / $nor. */
|
|
22
|
+
export type Query = {
|
|
23
|
+
$and?: Query[];
|
|
24
|
+
$or?: Query[];
|
|
25
|
+
$nor?: Query[];
|
|
26
|
+
[field: string]: unknown;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** Exclusion ({ email: 0 }) or inclusion ({ name: 1 }); only _id can mix both. */
|
|
30
|
+
export type Projection = Record<string, 0 | 1>;
|
|
31
|
+
|
|
32
|
+
export type UpdateFilter = {
|
|
33
|
+
$set?: Record<string, unknown>;
|
|
34
|
+
$unset?: Record<string, unknown>;
|
|
35
|
+
$inc?: Record<string, number>;
|
|
36
|
+
$push?: Record<string, unknown | { $each: unknown[] }>;
|
|
37
|
+
$pull?: Record<string, unknown>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export interface UpdateOptions {
|
|
41
|
+
/** Insert a document built from the filter when nothing matches. */
|
|
42
|
+
upsert?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface UpdateResult {
|
|
46
|
+
matchedCount: number;
|
|
47
|
+
modifiedCount: number;
|
|
48
|
+
upsertedId: unknown | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface DeleteResult {
|
|
52
|
+
deletedCount: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type ChangeType = 'insert' | 'update' | 'replace' | 'delete';
|
|
56
|
+
|
|
57
|
+
export interface ChangeEvent {
|
|
58
|
+
type: ChangeType;
|
|
59
|
+
ids: unknown[];
|
|
60
|
+
/** Copies of the documents (after the change; the removed ones for delete). */
|
|
61
|
+
documents: Document[];
|
|
62
|
+
/** Copies of the documents before an update or replace. */
|
|
63
|
+
previous?: Document[];
|
|
64
|
+
}
|
|
65
|
+
export type SortSpec = Record<string, 1 | -1>;
|
|
66
|
+
|
|
67
|
+
export interface SortOptions {
|
|
68
|
+
/** Intl.Collator options plus the locale, e.g. { locale: 'pt-BR', sensitivity: 'base', numeric: true } */
|
|
69
|
+
collation?: Intl.CollatorOptions & { locale?: string };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface StoredDocument {
|
|
73
|
+
readonly _data: Document;
|
|
74
|
+
readonly [field: string]: unknown;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export declare class Cursor {
|
|
78
|
+
readonly length: number;
|
|
79
|
+
currIndex: number;
|
|
80
|
+
hasNext(): boolean;
|
|
81
|
+
next(): StoredDocument;
|
|
82
|
+
reset(): void;
|
|
83
|
+
/** Data of the first document, null when empty. */
|
|
84
|
+
first(): Document | null;
|
|
85
|
+
/** Data of the last document, null when empty. */
|
|
86
|
+
last(): Document | null;
|
|
87
|
+
/** Plain copies of the documents. */
|
|
88
|
+
toArray(): Document[];
|
|
89
|
+
sort(spec: SortSpec, options?: SortOptions): this;
|
|
90
|
+
skip(count: number): this;
|
|
91
|
+
/** 0 means no limit. */
|
|
92
|
+
limit(count: number): this;
|
|
93
|
+
projection(projection: Projection): this;
|
|
94
|
+
push(element: object): void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export declare class Collection {
|
|
98
|
+
readonly first: Document;
|
|
99
|
+
readonly last: Document;
|
|
100
|
+
readonly all: StoredDocument[];
|
|
101
|
+
/** All documents, or the ones matching the query. */
|
|
102
|
+
count(query?: Query): number;
|
|
103
|
+
/** Inserts a copy of the document and returns its _id. */
|
|
104
|
+
insertOne(document: Document, callback?: (id: unknown) => void): unknown;
|
|
105
|
+
/** Validates every document first, then inserts them and returns their ids. */
|
|
106
|
+
insertMany(documents: Document[], callback?: (ids: unknown[]) => void): unknown[];
|
|
107
|
+
find(query: Query, projection?: Projection | null): Cursor;
|
|
108
|
+
/** Data of the first matching document, null when nothing matches. */
|
|
109
|
+
findOne(query: Query, projection?: Projection | null): Document | null;
|
|
110
|
+
updateOne(filter: Query, update: UpdateFilter, options?: UpdateOptions): UpdateResult;
|
|
111
|
+
updateMany(filter: Query, update: UpdateFilter, options?: UpdateOptions): UpdateResult;
|
|
112
|
+
/** Replaces the document, keeping its _id. */
|
|
113
|
+
replaceOne(filter: Query, replacement: Document, options?: UpdateOptions): UpdateResult;
|
|
114
|
+
deleteOne(filter: Query): DeleteResult;
|
|
115
|
+
/** deleteMany({}) empties the collection. */
|
|
116
|
+
deleteMany(filter: Query): DeleteResult;
|
|
117
|
+
/** Returns a function that unsubscribes. "change" receives every type. */
|
|
118
|
+
on(event: ChangeType | 'change', listener: (event: ChangeEvent) => void): () => void;
|
|
119
|
+
once(event: ChangeType | 'change', listener: (event: ChangeEvent) => void): () => void;
|
|
120
|
+
off(event: ChangeType | 'change', listener?: (event: ChangeEvent) => void): void;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface DatabaseStats {
|
|
124
|
+
name: string;
|
|
125
|
+
documents: number;
|
|
126
|
+
collections: Array<{ name: string; documents: number }>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export declare class Database {
|
|
130
|
+
readonly name: string;
|
|
131
|
+
createCollection(name: string, validationSchema?: unknown): void;
|
|
132
|
+
countCollections(): number;
|
|
133
|
+
listCollections(): string[];
|
|
134
|
+
/** Any collection by name, also when the name shadows a method. */
|
|
135
|
+
getCollection(name: string): Collection | undefined;
|
|
136
|
+
dropCollection(name: string): boolean;
|
|
137
|
+
getStats(): DatabaseStats;
|
|
138
|
+
[collection: string]: unknown;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
declare class sorla {
|
|
142
|
+
constructor();
|
|
143
|
+
/** The selected database. Collections are properties of it: sorla.db.people */
|
|
144
|
+
readonly db: Database & Record<string, Collection>;
|
|
145
|
+
dbExists(name: string): boolean;
|
|
146
|
+
/** Position of the new database, or false when the name exists. */
|
|
147
|
+
createDb(name: string): number | false;
|
|
148
|
+
/** Selects a database, creating it when needed. */
|
|
149
|
+
useDb(name: string): void;
|
|
150
|
+
countDb(): number;
|
|
151
|
+
listDatabases(): string[];
|
|
152
|
+
/** When the dropped database was selected, db becomes null. */
|
|
153
|
+
dropDb(name: string): boolean;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export { sorla };
|
|
157
|
+
export default sorla;
|
package/index.mjs
ADDED
package/package.json
CHANGED
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sorla-db",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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": "
|
|
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
|
}
|
package/src/dbs/database.js
CHANGED
|
@@ -67,6 +67,54 @@ class database
|
|
|
67
67
|
{
|
|
68
68
|
return this._collectionCount;
|
|
69
69
|
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Names of the collections, in creation order
|
|
73
|
+
* @return array
|
|
74
|
+
*/
|
|
75
|
+
listCollections()
|
|
76
|
+
{
|
|
77
|
+
return Object.keys(this._collections);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A collection by name, also when the name is not a valid property (or shadows a method)
|
|
82
|
+
* @param string name
|
|
83
|
+
* @return collection|undefined
|
|
84
|
+
*/
|
|
85
|
+
getCollection(name)
|
|
86
|
+
{
|
|
87
|
+
return this._collections.hasOwnProperty(name) ? this._collections[name] : undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Removes a collection and its documents
|
|
92
|
+
* @param string name
|
|
93
|
+
* @return boolean false when it did not exist
|
|
94
|
+
*/
|
|
95
|
+
dropCollection(name)
|
|
96
|
+
{
|
|
97
|
+
if(!this._collections.hasOwnProperty(name))
|
|
98
|
+
return false;
|
|
99
|
+
|
|
100
|
+
delete this._collections[name];
|
|
101
|
+
this._collectionCount--;
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Number of documents per collection
|
|
107
|
+
* @return object {name, documents, collections: [{name, documents}]}
|
|
108
|
+
*/
|
|
109
|
+
getStats()
|
|
110
|
+
{
|
|
111
|
+
const collections = this.listCollections().map(name => ({ name, documents: this._collections[name].count() }));
|
|
112
|
+
return {
|
|
113
|
+
name: this._name,
|
|
114
|
+
documents: collections.reduce((total, item) => total + item.documents, 0),
|
|
115
|
+
collections
|
|
116
|
+
};
|
|
117
|
+
}
|
|
70
118
|
}
|
|
71
119
|
|
|
72
120
|
module.exports = database;
|
package/src/documents/cursor.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
81
|
-
*
|
|
82
|
-
*
|
|
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
|
-
|
|
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
|
|
103
|
+
for(const key of keys)
|
|
88
104
|
{
|
|
89
|
-
const
|
|
90
|
-
if(
|
|
91
|
-
return
|
|
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
|
/**
|
|
@@ -108,23 +123,75 @@ class cursor {
|
|
|
108
123
|
}
|
|
109
124
|
|
|
110
125
|
/**
|
|
111
|
-
* Projection of the cursor
|
|
126
|
+
* Projection of the cursor, like MongoDB:
|
|
127
|
+
* {email: 0} every field except email
|
|
128
|
+
* {name: 1, age: 1} only name and age, plus _id unless {_id: 0}
|
|
129
|
+
* Inclusion and exclusion cannot be mixed, except for _id. Top level fields only.
|
|
112
130
|
* @param json projection
|
|
113
131
|
* @return cursor
|
|
114
132
|
*/
|
|
115
133
|
projection(projection) {
|
|
116
134
|
if(projection === null || typeof projection !== 'object')
|
|
117
|
-
return;
|
|
135
|
+
return this;
|
|
136
|
+
|
|
137
|
+
const fields = Object.keys(projection);
|
|
138
|
+
for(const field of fields)
|
|
139
|
+
{
|
|
140
|
+
if(projection[field] !== 0 && projection[field] !== 1)
|
|
141
|
+
throw new Error(`Projection of "${field}" must be 0 or 1`);
|
|
142
|
+
|
|
143
|
+
if(field.includes('.'))
|
|
144
|
+
throw new Error(`Projection supports top level fields only ("${field}")`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const included = fields.filter(field => projection[field] === 1 && field !== '_id');
|
|
148
|
+
const excluded = fields.filter(field => projection[field] === 0 && field !== '_id');
|
|
149
|
+
if(included.length && excluded.length)
|
|
150
|
+
throw new Error('Projection cannot mix inclusion and exclusion (except for _id)');
|
|
151
|
+
|
|
152
|
+
const keep = new Set(included);
|
|
153
|
+
if(projection._id !== 0) keep.add('_id');
|
|
118
154
|
|
|
119
|
-
const self = this;
|
|
120
155
|
this._data.forEach(doc => {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
156
|
+
const remove = included.length
|
|
157
|
+
? Object.keys(doc._data).filter(field => !keep.has(field))
|
|
158
|
+
: [...excluded, ...(projection._id === 0 ? ['_id'] : [])];
|
|
159
|
+
|
|
160
|
+
for(const field of remove)
|
|
161
|
+
doc.deleteProp(field);
|
|
127
162
|
});
|
|
163
|
+
|
|
164
|
+
return this;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Skips the first documents (apply after sort, like MongoDB applies it)
|
|
169
|
+
* @param int count
|
|
170
|
+
* @return cursor
|
|
171
|
+
*/
|
|
172
|
+
skip(count) {
|
|
173
|
+
if(!Number.isInteger(count) || count < 0)
|
|
174
|
+
throw new Error('skip() needs a non negative integer');
|
|
175
|
+
|
|
176
|
+
this._data = this._data.slice(count);
|
|
177
|
+
this.currIndex = 0;
|
|
178
|
+
return this;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Keeps at most `count` documents; 0 means no limit, like MongoDB
|
|
183
|
+
* @param int count
|
|
184
|
+
* @return cursor
|
|
185
|
+
*/
|
|
186
|
+
limit(count) {
|
|
187
|
+
if(!Number.isInteger(count) || count < 0)
|
|
188
|
+
throw new Error('limit() needs a non negative integer');
|
|
189
|
+
|
|
190
|
+
if(count > 0)
|
|
191
|
+
this._data = this._data.slice(0, count);
|
|
192
|
+
|
|
193
|
+
this.currIndex = 0;
|
|
194
|
+
return this;
|
|
128
195
|
}
|
|
129
196
|
|
|
130
197
|
/**
|
|
@@ -141,4 +208,53 @@ class cursor {
|
|
|
141
208
|
}
|
|
142
209
|
}
|
|
143
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Rank of a value in MongoDB's comparison order
|
|
213
|
+
* @param mixed value
|
|
214
|
+
* @return number
|
|
215
|
+
*/
|
|
216
|
+
function typeRank(value) {
|
|
217
|
+
if(value === undefined || value === null) return 0;
|
|
218
|
+
if(typeof value === 'number') return 1;
|
|
219
|
+
if(typeof value === 'string') return 2;
|
|
220
|
+
// toString instead of instanceof: Dates from another realm (iframes, workers, vm) still count
|
|
221
|
+
if(Object.prototype.toString.call(value) === '[object Date]') return 6;
|
|
222
|
+
if(Array.isArray(value)) return 4;
|
|
223
|
+
if(typeof value === 'object') return 3;
|
|
224
|
+
if(typeof value === 'boolean') return 5;
|
|
225
|
+
return 7;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Compares two field values for sorting
|
|
230
|
+
* @param mixed a
|
|
231
|
+
* @param mixed b
|
|
232
|
+
* @param Intl.Collator|null collation
|
|
233
|
+
* @return number -1, 0 or 1
|
|
234
|
+
*/
|
|
235
|
+
function compareValues(a, b, collation) {
|
|
236
|
+
const rankA = typeRank(a);
|
|
237
|
+
const rankB = typeRank(b);
|
|
238
|
+
if(rankA !== rankB) return rankA < rankB ? -1 : 1;
|
|
239
|
+
|
|
240
|
+
switch(rankA) {
|
|
241
|
+
case 0:
|
|
242
|
+
return 0;
|
|
243
|
+
case 2:
|
|
244
|
+
if(collation) return Math.sign(collation.compare(a, b));
|
|
245
|
+
break;
|
|
246
|
+
case 3:
|
|
247
|
+
case 4:
|
|
248
|
+
a = JSON.stringify(a);
|
|
249
|
+
b = JSON.stringify(b);
|
|
250
|
+
break;
|
|
251
|
+
case 6:
|
|
252
|
+
a = a.getTime();
|
|
253
|
+
b = b.getTime();
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
144
260
|
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
|
|
16
|
-
if(target._cloned && target.
|
|
17
|
+
deleteProperty: (target, prop) => {
|
|
18
|
+
if(target._cloned && prop in target._data)
|
|
17
19
|
{
|
|
18
|
-
|
|
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
|
|
40
|
-
if(
|
|
41
|
-
|
|
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(
|
|
49
|
-
|
|
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
|
|
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
|
|
74
|
+
deleteProp(prop)
|
|
78
75
|
{
|
|
79
|
-
this.
|
|
80
|
-
|
|
81
|
-
return Reflect.deleteProperty(this, prop);
|
|
76
|
+
if(!this._cloned)
|
|
77
|
+
return false;
|
|
82
78
|
|
|
83
|
-
return
|
|
79
|
+
return Reflect.deleteProperty(this, prop);
|
|
84
80
|
}
|
|
85
81
|
}
|
|
86
82
|
|
package/src/events.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal event emitter for collections: insert, update, replace, delete and change (all of them).
|
|
3
|
+
* A failing listener never breaks the write that triggered it; its error is rethrown asynchronously.
|
|
4
|
+
*/
|
|
5
|
+
class emitter
|
|
6
|
+
{
|
|
7
|
+
constructor()
|
|
8
|
+
{
|
|
9
|
+
this._listeners = new Map();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param string event
|
|
14
|
+
* @param function listener receives the event payload
|
|
15
|
+
* @return function unsubscribe
|
|
16
|
+
*/
|
|
17
|
+
on(event, listener)
|
|
18
|
+
{
|
|
19
|
+
if(typeof listener !== 'function')
|
|
20
|
+
throw new Error('The listener must be a function');
|
|
21
|
+
|
|
22
|
+
if(!this._listeners.has(event))
|
|
23
|
+
this._listeners.set(event, new Set());
|
|
24
|
+
|
|
25
|
+
this._listeners.get(event).add(listener);
|
|
26
|
+
return () => this.off(event, listener);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
once(event, listener)
|
|
30
|
+
{
|
|
31
|
+
const off = this.on(event, payload => {
|
|
32
|
+
off();
|
|
33
|
+
listener(payload);
|
|
34
|
+
});
|
|
35
|
+
return off;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
off(event, listener)
|
|
39
|
+
{
|
|
40
|
+
if(typeof listener === 'undefined')
|
|
41
|
+
this._listeners.delete(event);
|
|
42
|
+
else
|
|
43
|
+
this._listeners.get(event)?.delete(listener);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Emits the event and a generic "change" event with the same payload plus its type
|
|
48
|
+
* @param string type
|
|
49
|
+
* @param object payload
|
|
50
|
+
* @return void
|
|
51
|
+
*/
|
|
52
|
+
_emitChange(type, payload)
|
|
53
|
+
{
|
|
54
|
+
const event = { type, ...payload };
|
|
55
|
+
for(const name of [type, 'change'])
|
|
56
|
+
{
|
|
57
|
+
for(const listener of [...(this._listeners.get(name) ?? [])])
|
|
58
|
+
{
|
|
59
|
+
try {
|
|
60
|
+
listener(event);
|
|
61
|
+
} catch(error) {
|
|
62
|
+
setTimeout(() => { throw error; });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = emitter;
|