hola-server 1.0.11 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +196 -1
- package/core/array.js +79 -142
- package/core/bash.js +208 -259
- package/core/chart.js +26 -16
- package/core/cron.js +14 -3
- package/core/date.js +15 -44
- package/core/encrypt.js +19 -9
- package/core/file.js +42 -29
- package/core/lhs.js +32 -6
- package/core/meta.js +213 -289
- package/core/msg.js +20 -7
- package/core/number.js +105 -103
- package/core/obj.js +15 -12
- package/core/random.js +9 -6
- package/core/role.js +69 -77
- package/core/thread.js +12 -2
- package/core/type.js +300 -261
- package/core/url.js +20 -12
- package/core/validate.js +29 -26
- package/db/db.js +297 -227
- package/db/entity.js +631 -963
- package/db/gridfs.js +120 -166
- package/design/add_default_field_attr.md +56 -0
- package/http/context.js +22 -8
- package/http/cors.js +25 -8
- package/http/error.js +27 -9
- package/http/express.js +70 -41
- package/http/params.js +70 -42
- package/http/router.js +51 -40
- package/http/session.js +59 -36
- package/index.js +85 -9
- package/package.json +2 -2
- package/router/clone.js +28 -36
- package/router/create.js +21 -26
- package/router/delete.js +24 -28
- package/router/read.js +137 -123
- package/router/update.js +38 -56
- package/setting.js +22 -6
- package/skills/array.md +155 -0
- package/skills/bash.md +91 -0
- package/skills/chart.md +54 -0
- package/skills/code.md +422 -0
- package/skills/context.md +177 -0
- package/skills/date.md +58 -0
- package/skills/express.md +255 -0
- package/skills/file.md +60 -0
- package/skills/lhs.md +54 -0
- package/skills/meta.md +1023 -0
- package/skills/msg.md +30 -0
- package/skills/number.md +88 -0
- package/skills/obj.md +36 -0
- package/skills/params.md +206 -0
- package/skills/random.md +22 -0
- package/skills/role.md +59 -0
- package/skills/session.md +281 -0
- package/skills/storage.md +743 -0
- package/skills/thread.md +22 -0
- package/skills/type.md +547 -0
- package/skills/url.md +34 -0
- package/skills/validate.md +48 -0
- package/test/cleanup/close-db.js +5 -0
- package/test/core/array.js +226 -0
- package/test/core/chart.js +51 -0
- package/test/core/file.js +59 -0
- package/test/core/lhs.js +44 -0
- package/test/core/number.js +167 -12
- package/test/core/obj.js +47 -0
- package/test/core/random.js +24 -0
- package/test/core/thread.js +20 -0
- package/test/core/type.js +216 -0
- package/test/core/validate.js +67 -0
- package/test/db/db-ops.js +99 -0
- package/test/db/pipe_test.txt +0 -0
- package/test/db/test_case_design.md +528 -0
- package/test/db/test_db_class.js +613 -0
- package/test/db/test_entity_class.js +414 -0
- package/test/db/test_gridfs_class.js +234 -0
- package/test/entity/create.js +1 -1
- package/test/entity/delete-mixed.js +156 -0
- package/test/entity/ref-filter.js +63 -0
- package/tool/gen_i18n.js +55 -21
- package/test/crud/router.js +0 -99
- package/test/router/user.js +0 -17
package/README.md
CHANGED
|
@@ -1 +1,196 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Hola Server
|
|
2
|
+
|
|
3
|
+
A meta-programming framework for building Node.js RESTful APIs with MongoDB. Hola Server provides a declarative, metadata-driven approach to building CRUD APIs with built-in authentication, role-based access control, and file handling.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Meta-Driven CRUD**: Define entity schemas with metadata and get full CRUD APIs automatically
|
|
8
|
+
- **Role-Based Access Control**: Fine-grained permissions per entity, operation, and view
|
|
9
|
+
- **Type System**: Extensible type conversion and validation system
|
|
10
|
+
- **File Handling**: Built-in GridFS integration for file uploads
|
|
11
|
+
- **Session Management**: Express session with MongoDB store
|
|
12
|
+
- **Entity Relationships**: Support for references with cascade/keep delete behavior
|
|
13
|
+
- **Query Building**: Advanced search, filtering, and pagination
|
|
14
|
+
- **Async Context**: Request-scoped context using AsyncLocalStorage
|
|
15
|
+
- **Comprehensive Testing**: Full test suite with 111+ passing tests
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### 1. Configure Settings
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
const { init_settings } = require("hola-server");
|
|
29
|
+
|
|
30
|
+
init_settings({
|
|
31
|
+
mongo: {
|
|
32
|
+
url: "mongodb://localhost/myapp",
|
|
33
|
+
pool: 10,
|
|
34
|
+
},
|
|
35
|
+
server: {
|
|
36
|
+
service_port: 8088,
|
|
37
|
+
client_web_url: ["http://localhost:3000"],
|
|
38
|
+
session: {
|
|
39
|
+
secret: "your-secret-key",
|
|
40
|
+
cookie_max_age: 86400000, // 1 day
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
roles: [{ name: "admin", root: true }, { name: "user" }],
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. Define Entity Metadata
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
const { EntityMeta } = require("hola-server");
|
|
51
|
+
|
|
52
|
+
const user_meta = {
|
|
53
|
+
collection: "user",
|
|
54
|
+
mode: "crud",
|
|
55
|
+
fields: [
|
|
56
|
+
{ name: "name", type: "string", required: true },
|
|
57
|
+
{ name: "email", type: "string", required: true, primary: true },
|
|
58
|
+
{ name: "age", type: "uint" },
|
|
59
|
+
{ name: "role", type: "obj", ref: "role" },
|
|
60
|
+
],
|
|
61
|
+
roles: ["admin:crud:*", "user:r:*"],
|
|
62
|
+
};
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 3. Create Router
|
|
66
|
+
|
|
67
|
+
```javascript
|
|
68
|
+
const { init_router } = require("hola-server");
|
|
69
|
+
|
|
70
|
+
const router = init_router(user_meta);
|
|
71
|
+
module.exports = router;
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 4. Start Server
|
|
75
|
+
|
|
76
|
+
```javascript
|
|
77
|
+
const { init_express_server } = require("hola-server");
|
|
78
|
+
|
|
79
|
+
init_express_server(__dirname, "service_port", async () => {
|
|
80
|
+
console.log("Server started on port 8088");
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Entity Metadata
|
|
85
|
+
|
|
86
|
+
### Field Types
|
|
87
|
+
|
|
88
|
+
- `string` - String with trim
|
|
89
|
+
- `int` - Integer
|
|
90
|
+
- `uint` - Unsigned integer
|
|
91
|
+
- `float` - Float with 2 decimal places
|
|
92
|
+
- `ufloat` - Unsigned float
|
|
93
|
+
- `number` - Any number
|
|
94
|
+
- `boolean` - Boolean
|
|
95
|
+
- `password` - Encrypted password
|
|
96
|
+
- `array` - Array type
|
|
97
|
+
- `obj` - Object/Reference type
|
|
98
|
+
- `file` - File upload
|
|
99
|
+
|
|
100
|
+
### Field Options
|
|
101
|
+
|
|
102
|
+
- `required` - Field is required
|
|
103
|
+
- `primary` - Primary key (unique)
|
|
104
|
+
- `searchable` - Searchable in queries
|
|
105
|
+
- `invisible` - Hidden from responses
|
|
106
|
+
- `sys` - System field
|
|
107
|
+
- `ref` - Reference to another entity
|
|
108
|
+
- `delete` - Cascade behavior: `cascade` or `keep`
|
|
109
|
+
|
|
110
|
+
### CRUD Modes
|
|
111
|
+
|
|
112
|
+
- `c` - Create
|
|
113
|
+
- `r` - Read
|
|
114
|
+
- `u` - Update
|
|
115
|
+
- `d` - Delete
|
|
116
|
+
- `o` - Clone
|
|
117
|
+
- `crud` - All operations
|
|
118
|
+
|
|
119
|
+
### Role Configuration
|
|
120
|
+
|
|
121
|
+
Format: `role_name:mode:view`
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
|
|
125
|
+
```javascript
|
|
126
|
+
roles: [
|
|
127
|
+
"admin:crud:*", // Admin can do all operations on all views
|
|
128
|
+
"user:r:public", // User can only read public view
|
|
129
|
+
"editor:cru:edit", // Editor can create/read/update edit view
|
|
130
|
+
];
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## API Endpoints
|
|
134
|
+
|
|
135
|
+
For an entity with `mode: 'crud'`:
|
|
136
|
+
|
|
137
|
+
- `POST /{entity}/create` - Create entity
|
|
138
|
+
- `POST /{entity}/query` - Query entities
|
|
139
|
+
- `POST /{entity}/count` - Count entities
|
|
140
|
+
- `POST /{entity}/update` - Update entity
|
|
141
|
+
- `POST /{entity}/delete` - Delete entity
|
|
142
|
+
- `POST /{entity}/clone` - Clone entity (if cloneable)
|
|
143
|
+
|
|
144
|
+
## Core Utilities
|
|
145
|
+
|
|
146
|
+
### Database
|
|
147
|
+
|
|
148
|
+
```javascript
|
|
149
|
+
const { get_db } = require("hola-server");
|
|
150
|
+
|
|
151
|
+
const db = await get_db();
|
|
152
|
+
const users = await db.collection("user").find({});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Entity Operations
|
|
156
|
+
|
|
157
|
+
```javascript
|
|
158
|
+
const { Entity } = require("hola-server");
|
|
159
|
+
|
|
160
|
+
const entity = new Entity(meta);
|
|
161
|
+
await entity.create_entity(data);
|
|
162
|
+
await entity.find_entity(query);
|
|
163
|
+
await entity.update_entity(id, updates);
|
|
164
|
+
await entity.delete_entity(id);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Logging
|
|
168
|
+
|
|
169
|
+
```javascript
|
|
170
|
+
const { log_info, log_error } = require("hola-server");
|
|
171
|
+
|
|
172
|
+
log_info("user", "User logged in", { user_id });
|
|
173
|
+
log_error("auth", "Login failed", { email });
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Testing
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
npm test
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Project Structure
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
hola-server/
|
|
186
|
+
├── core/ # Core utilities (array, date, validate, etc.)
|
|
187
|
+
├── db/ # Database layer (connection, entity, gridfs)
|
|
188
|
+
├── http/ # HTTP layer (express, router, middleware)
|
|
189
|
+
├── router/ # CRUD route handlers
|
|
190
|
+
├── test/ # Test suite
|
|
191
|
+
└── tool/ # Tools (i18n generation)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
ISC
|
package/core/array.js
CHANGED
|
@@ -1,187 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Array manipulation utility functions.
|
|
3
|
+
* @module core/array
|
|
4
|
+
*/
|
|
5
|
+
|
|
1
6
|
const { round_to_fixed2 } = require('./number');
|
|
2
7
|
|
|
3
8
|
/**
|
|
4
|
-
* Shuffle
|
|
5
|
-
*
|
|
6
|
-
* @param {array to be shuffle} a
|
|
9
|
+
* Shuffle array elements randomly in place.
|
|
10
|
+
* @param {Array} arr - Array to shuffle.
|
|
7
11
|
*/
|
|
8
|
-
const shuffle = (
|
|
9
|
-
let
|
|
10
|
-
|
|
11
|
-
j =
|
|
12
|
-
x = a[i - 1];
|
|
13
|
-
a[i - 1] = a[j];
|
|
14
|
-
a[j] = x;
|
|
12
|
+
const shuffle = (arr) => {
|
|
13
|
+
for (let i = arr.length; i; i--) {
|
|
14
|
+
const j = Math.floor(Math.random() * i);
|
|
15
|
+
[arr[i - 1], arr[j]] = [arr[j], arr[i - 1]];
|
|
15
16
|
}
|
|
16
|
-
}
|
|
17
|
+
};
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
|
-
* Remove
|
|
20
|
-
* @param {array of
|
|
21
|
-
* @param {
|
|
22
|
-
* @param {
|
|
20
|
+
* Remove elements from array by matching field value.
|
|
21
|
+
* @param {Object[]} array - Array of objects to modify.
|
|
22
|
+
* @param {string} field - Field name to match.
|
|
23
|
+
* @param {*} value - Value to match for removal.
|
|
23
24
|
*/
|
|
24
25
|
const remove_element = (array, field, value) => {
|
|
25
26
|
for (let i = array.length - 1; i >= 0; --i) {
|
|
26
|
-
if (array[i][field] == value)
|
|
27
|
-
array.splice(i, 1);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Pop n elements from the array
|
|
34
|
-
* @param {array of elements} array
|
|
35
|
-
* @param {n number of object to pop} n
|
|
36
|
-
* @returns
|
|
37
|
-
*/
|
|
38
|
-
const pop_n = (array, n) => {
|
|
39
|
-
const subarray = [];
|
|
40
|
-
while (subarray.length < n) {
|
|
41
|
-
const ele = array.pop();
|
|
42
|
-
if (ele) {
|
|
43
|
-
subarray.push(ele);
|
|
44
|
-
} else {
|
|
45
|
-
break;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if (subarray.length > 0) {
|
|
49
|
-
return subarray;
|
|
50
|
-
} else {
|
|
51
|
-
return undefined;
|
|
27
|
+
if (array[i][field] == value) array.splice(i, 1);
|
|
52
28
|
}
|
|
53
|
-
}
|
|
29
|
+
};
|
|
54
30
|
|
|
55
31
|
/**
|
|
56
|
-
*
|
|
57
|
-
* @param {array
|
|
58
|
-
* @param {n
|
|
59
|
-
* @
|
|
32
|
+
* Extract n elements from array using specified method.
|
|
33
|
+
* @param {Array} array - Source array.
|
|
34
|
+
* @param {number} n - Number of elements to extract.
|
|
35
|
+
* @param {string} method - 'pop' or 'shift'.
|
|
36
|
+
* @returns {Array|undefined} Array of extracted elements or undefined if empty.
|
|
60
37
|
*/
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
while (
|
|
64
|
-
const ele = array
|
|
65
|
-
if (ele)
|
|
66
|
-
|
|
67
|
-
} else {
|
|
68
|
-
break;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
if (subarray.length > 0) {
|
|
72
|
-
return subarray;
|
|
73
|
-
} else {
|
|
74
|
-
return undefined;
|
|
38
|
+
const extract_n = (array, n, method) => {
|
|
39
|
+
const result = [];
|
|
40
|
+
while (result.length < n) {
|
|
41
|
+
const ele = array[method]();
|
|
42
|
+
if (ele) result.push(ele);
|
|
43
|
+
else break;
|
|
75
44
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Calculate the sum value of the array of the number
|
|
80
|
-
* @param {array of number} arr
|
|
81
|
-
* @returns
|
|
82
|
-
*/
|
|
83
|
-
const sum = function (arr) {
|
|
84
|
-
return round_to_fixed2(arr.reduce((pre, cur) => pre + cur));
|
|
45
|
+
return result.length > 0 ? result : undefined;
|
|
85
46
|
};
|
|
86
47
|
|
|
48
|
+
const pop_n = (array, n) => extract_n(array, n, 'pop');
|
|
49
|
+
const shift_n = (array, n) => extract_n(array, n, 'shift');
|
|
50
|
+
|
|
87
51
|
/**
|
|
88
|
-
* Calculate
|
|
89
|
-
* @param {
|
|
90
|
-
* @returns
|
|
52
|
+
* Calculate sum of number array.
|
|
53
|
+
* @param {number[]} arr - Array of numbers.
|
|
54
|
+
* @returns {number} Sum rounded to 2 decimal places.
|
|
91
55
|
*/
|
|
92
|
-
const
|
|
93
|
-
if (arr.length === 0) {
|
|
94
|
-
return 0;
|
|
95
|
-
} else {
|
|
96
|
-
return round_to_fixed2(sum(arr) / arr.length);
|
|
97
|
-
}
|
|
98
|
-
};
|
|
56
|
+
const sum = (arr) => round_to_fixed2(arr.reduce((pre, cur) => pre + cur, 0));
|
|
99
57
|
|
|
100
58
|
/**
|
|
101
|
-
*
|
|
102
|
-
* @param {
|
|
103
|
-
* @
|
|
104
|
-
* @param {value attr of element as value} value_attr
|
|
105
|
-
* @returns
|
|
59
|
+
* Calculate average of number array.
|
|
60
|
+
* @param {number[]} arr - Array of numbers.
|
|
61
|
+
* @returns {number} Average rounded to 2 decimal places.
|
|
106
62
|
*/
|
|
107
|
-
const
|
|
108
|
-
const obj = {};
|
|
109
|
-
arr.forEach(element => {
|
|
110
|
-
obj[element[key_attr]] = element[value_attr];
|
|
111
|
-
});
|
|
112
|
-
return obj;
|
|
113
|
-
}
|
|
63
|
+
const avg = (arr) => arr.length === 0 ? 0 : round_to_fixed2(sum(arr) / arr.length);
|
|
114
64
|
|
|
115
65
|
/**
|
|
116
|
-
*
|
|
117
|
-
* @param {
|
|
118
|
-
* @param {
|
|
119
|
-
* @
|
|
66
|
+
* Convert array of objects to key-value object.
|
|
67
|
+
* @param {Object[]} arr - Array of objects.
|
|
68
|
+
* @param {string} key_attr - Attribute to use as key.
|
|
69
|
+
* @param {string} value_attr - Attribute to use as value.
|
|
70
|
+
* @returns {Object} Mapped object.
|
|
120
71
|
*/
|
|
121
|
-
const
|
|
122
|
-
arr.
|
|
123
|
-
return b[attr] - a[attr];
|
|
124
|
-
});
|
|
125
|
-
return arr;
|
|
72
|
+
const map_array_to_obj = (arr, key_attr, value_attr) => {
|
|
73
|
+
return arr.reduce((obj, el) => ({ ...obj, [el[key_attr]]: el[value_attr] }), {});
|
|
126
74
|
};
|
|
127
75
|
|
|
128
76
|
/**
|
|
129
|
-
* Sort array of
|
|
130
|
-
* @param {
|
|
131
|
-
* @param {
|
|
132
|
-
* @
|
|
77
|
+
* Sort array of objects by attribute.
|
|
78
|
+
* @param {Object[]} arr - Array to sort.
|
|
79
|
+
* @param {string} attr - Attribute name to sort by.
|
|
80
|
+
* @param {boolean} [desc=false] - Sort descending if true.
|
|
81
|
+
* @returns {Object[]} Sorted array.
|
|
133
82
|
*/
|
|
134
|
-
const
|
|
135
|
-
arr.sort((a, b) =>
|
|
136
|
-
return a[attr] - b[attr];
|
|
137
|
-
});
|
|
83
|
+
const sort_by_attr = (arr, attr, desc = false) => {
|
|
84
|
+
arr.sort((a, b) => desc ? b[attr] - a[attr] : a[attr] - b[attr]);
|
|
138
85
|
return arr;
|
|
139
86
|
};
|
|
140
87
|
|
|
88
|
+
const sort_desc = (arr, attr) => sort_by_attr(arr, attr, true);
|
|
89
|
+
const sort_asc = (arr, attr) => sort_by_attr(arr, attr, false);
|
|
90
|
+
|
|
141
91
|
/**
|
|
142
|
-
*
|
|
143
|
-
* @param {
|
|
144
|
-
* @param {
|
|
145
|
-
* @param {
|
|
146
|
-
* @returns
|
|
92
|
+
* Sort array by predefined key sequence.
|
|
93
|
+
* @param {Object[]} arr - Array to sort.
|
|
94
|
+
* @param {string} attr - Attribute name to sort by.
|
|
95
|
+
* @param {Array} keys - Ordered array of key values.
|
|
96
|
+
* @returns {Object[]} Sorted array.
|
|
147
97
|
*/
|
|
148
|
-
const sort_by_key_seq =
|
|
149
|
-
arr.sort((a, b) =>
|
|
150
|
-
return keys.indexOf(a[attr]) - keys.indexOf(b[attr]);
|
|
151
|
-
});
|
|
98
|
+
const sort_by_key_seq = (arr, attr, keys) => {
|
|
99
|
+
arr.sort((a, b) => keys.indexOf(a[attr]) - keys.indexOf(b[attr]));
|
|
152
100
|
return arr;
|
|
153
101
|
};
|
|
154
102
|
|
|
155
103
|
/**
|
|
156
|
-
* Create
|
|
157
|
-
* @param {
|
|
158
|
-
* @param {
|
|
159
|
-
* @returns
|
|
104
|
+
* Create cartesian product of two object arrays.
|
|
105
|
+
* @param {Object[]} arr1 - First array.
|
|
106
|
+
* @param {Object[]} arr2 - Second array.
|
|
107
|
+
* @returns {Object[]} Combined array with length arr1.length * arr2.length.
|
|
160
108
|
*/
|
|
161
|
-
const combine = (arr1, arr2) => {
|
|
162
|
-
const result = [];
|
|
163
|
-
for (let i = 0; i < arr1.length; i++) {
|
|
164
|
-
const obj1 = arr1[i];
|
|
165
|
-
for (let j = 0; j < arr2.length; j++) {
|
|
166
|
-
const obj2 = arr2[j];
|
|
167
|
-
result.push({ ...obj1, ...obj2 });
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
return result;
|
|
171
|
-
}
|
|
109
|
+
const combine = (arr1, arr2) => arr1.flatMap(obj1 => arr2.map(obj2 => ({ ...obj1, ...obj2 })));
|
|
172
110
|
|
|
173
111
|
/**
|
|
174
|
-
* Remove duplicate
|
|
175
|
-
* @param {array
|
|
176
|
-
* @returns
|
|
112
|
+
* Remove duplicate elements from array.
|
|
113
|
+
* @param {Array} array - Array to deduplicate.
|
|
114
|
+
* @returns {Array} Array with unique elements.
|
|
177
115
|
*/
|
|
178
116
|
const unique = (array) => {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
});
|
|
117
|
+
const seen = new Map();
|
|
118
|
+
return array.filter(value => {
|
|
119
|
+
const key = JSON.stringify(value);
|
|
120
|
+
return seen.has(key) ? false : (seen.set(key, true), true);
|
|
184
121
|
});
|
|
185
|
-
}
|
|
122
|
+
};
|
|
186
123
|
|
|
187
|
-
module.exports = { shuffle, remove_element, pop_n, shift_n, sum, avg, combine, sort_desc, sort_asc, sort_by_key_seq, unique, map_array_to_obj }
|
|
124
|
+
module.exports = { shuffle, remove_element, pop_n, shift_n, sum, avg, combine, sort_desc, sort_asc, sort_by_key_seq, unique, map_array_to_obj };
|