spice-js 2.6.70 → 2.6.71
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/build/bootstrap/map.js +17 -29
- package/build/bootstrap/routes.js +45 -9
- package/build/bootstrap/schema_extenders.js +38 -9
- package/build/bootstrap/validation.js +38 -25
- package/build/index.js +72 -6
- package/build/models/SpiceModel.js +83 -23
- package/bun.lock +2281 -0
- package/package.json +1 -1
- package/src/bootstrap/map.js +17 -12
- package/src/bootstrap/routes.js +36 -10
- package/src/bootstrap/schema_extenders.js +33 -10
- package/src/bootstrap/validation.js +38 -19
- package/src/index.js +51 -4
- package/src/models/SpiceModel.js +64 -17
package/package.json
CHANGED
package/src/bootstrap/map.js
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import _ from "lodash";
|
|
3
|
+
|
|
3
4
|
try {
|
|
4
5
|
let resources = ["models", "controllers", "schemas", "cache"];
|
|
5
|
-
|
|
6
|
+
|
|
7
|
+
for (const resource of resources) {
|
|
6
8
|
let paths = path.join(spice.root_path, resource);
|
|
7
9
|
spice[resource] = {};
|
|
8
|
-
|
|
10
|
+
|
|
11
|
+
const files = require("fs").readdirSync(paths);
|
|
12
|
+
|
|
13
|
+
for (let file of files) {
|
|
9
14
|
let file_name = file.split(".")[0];
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
15
|
+
const imported = require(path.join(paths, file));
|
|
16
|
+
const moduleDefault = imported.default || imported;
|
|
17
|
+
|
|
18
|
+
// ⚡ Only store once, create both references to same object
|
|
19
|
+
spice[resource][file_name] = moduleDefault;
|
|
20
|
+
spice[resource][file_name.toLowerCase()] = moduleDefault;
|
|
21
|
+
|
|
22
|
+
// ⚡ REMOVED: No longer instantiating models at startup
|
|
23
|
+
// Models will be instantiated when needed via `new spice.models[name]({})`
|
|
19
24
|
}
|
|
20
|
-
}
|
|
25
|
+
}
|
|
21
26
|
} catch (error) {
|
|
22
27
|
console.log("Error Happened", error);
|
|
23
28
|
}
|
package/src/bootstrap/routes.js
CHANGED
|
@@ -1,16 +1,42 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { promisify } from 'util';
|
|
2
4
|
|
|
5
|
+
const readdir = promisify(fs.readdir);
|
|
3
6
|
|
|
4
|
-
spice.routers = {}
|
|
7
|
+
spice.routers = {};
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// ⚡ OPTIMIZED: Async file operations for non-blocking route loading
|
|
10
|
+
async function loadRoutes() {
|
|
11
|
+
let paths = path.join(spice.root_path, 'routes');
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const files = await readdir(paths);
|
|
15
|
+
|
|
16
|
+
for (const file of files) {
|
|
17
|
+
if (file !== 'index.js' && file.endsWith('.js')) {
|
|
18
|
+
try {
|
|
19
|
+
let router = require(path.join(paths, file));
|
|
20
|
+
|
|
21
|
+
// Handle ESM default exports
|
|
22
|
+
if (router.default) {
|
|
23
|
+
router = router.default;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const routerName = file.split('.')[0];
|
|
27
|
+
spice.routers[routerName] = router;
|
|
11
28
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
29
|
+
spice.app
|
|
30
|
+
.use(spice.routers[routerName].routes())
|
|
31
|
+
.use(spice.routers[routerName].allowedMethods());
|
|
32
|
+
} catch (routeError) {
|
|
33
|
+
console.error(`Failed to load route ${file}:`, routeError.message);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Error loading routes:', error);
|
|
15
39
|
}
|
|
16
|
-
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = loadRoutes;
|
|
@@ -1,16 +1,39 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs";
|
|
3
|
-
|
|
3
|
+
import { promisify } from "util";
|
|
4
|
+
|
|
5
|
+
const readdir = promisify(fs.readdir);
|
|
6
|
+
|
|
4
7
|
spice.schema_extenders = {};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
|
|
9
|
+
// ⚡ OPTIMIZED: Async file operations for non-blocking schema extender loading
|
|
10
|
+
async function loadSchemaExtenders() {
|
|
11
|
+
let paths = path.join(spice.root_path, "schema_extenders");
|
|
12
|
+
|
|
13
|
+
if (fs.existsSync(paths)) {
|
|
14
|
+
try {
|
|
15
|
+
const files = await readdir(paths);
|
|
16
|
+
|
|
17
|
+
for (const file of files) {
|
|
18
|
+
if (path.extname(file) === ".js" && file !== "index.js") {
|
|
19
|
+
try {
|
|
20
|
+
let extender = require(path.join(paths, file));
|
|
21
|
+
|
|
22
|
+
// Handle ESM default exports
|
|
23
|
+
if (extender.default) {
|
|
24
|
+
extender = extender.default;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
spice.schema_extenders[path.basename(file, ".js")] = extender;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error(`Failed to load schema extender ${file}:`, error.message);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
13
32
|
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error("Error loading schema extenders:", error);
|
|
14
35
|
}
|
|
15
|
-
}
|
|
36
|
+
}
|
|
16
37
|
}
|
|
38
|
+
|
|
39
|
+
module.exports = loadSchemaExtenders;
|
|
@@ -2,25 +2,44 @@
|
|
|
2
2
|
* Created by chadfraser on 25/02/2017.
|
|
3
3
|
*/
|
|
4
4
|
import path from 'path';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import { promisify } from 'util';
|
|
7
|
+
|
|
8
|
+
const readdir = promisify(fs.readdir);
|
|
9
|
+
|
|
10
|
+
// ⚡ OPTIMIZED: Async file operations for non-blocking validator loading
|
|
11
|
+
module.exports = async function () {
|
|
12
|
+
try {
|
|
13
|
+
let locations = [
|
|
14
|
+
spice.module_root_path + '/val',
|
|
15
|
+
spice.root_path + '/validators'
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
for (let location of locations) {
|
|
19
|
+
// Check if directory exists before trying to read it
|
|
20
|
+
if (fs.existsSync(location)) {
|
|
21
|
+
try {
|
|
22
|
+
const files = await readdir(location);
|
|
23
|
+
|
|
24
|
+
for (const file of files) {
|
|
25
|
+
if (file !== 'index.js' && file.endsWith('.js')) {
|
|
26
|
+
try {
|
|
27
|
+
require(path.join(location, file));
|
|
28
|
+
console.log('Loading Validator', path.join(location, file));
|
|
29
|
+
} catch (validatorError) {
|
|
30
|
+
console.error(`Failed to load validator ${file}:`, validatorError.message);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
17
33
|
}
|
|
18
|
-
})
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.log(`Error reading ${location}:`, error.message);
|
|
36
|
+
}
|
|
19
37
|
}
|
|
20
|
-
resolve({})
|
|
21
|
-
|
|
22
|
-
} catch (e) {
|
|
23
|
-
reject(e);
|
|
24
38
|
}
|
|
25
|
-
|
|
26
|
-
}
|
|
39
|
+
|
|
40
|
+
return {};
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.error('Error in validation bootstrap:', e);
|
|
43
|
+
throw e;
|
|
44
|
+
}
|
|
45
|
+
};
|
package/src/index.js
CHANGED
|
@@ -96,21 +96,68 @@ export default class Spice {
|
|
|
96
96
|
/* app._io.on("connection", (sock) => {
|
|
97
97
|
console.log("Connection Up", sock);
|
|
98
98
|
}); */
|
|
99
|
+
|
|
100
|
+
// ⚡ OPTIMIZED: Load routes and models first, then generate docs lazily
|
|
101
|
+
await require("./loaders").load();
|
|
102
|
+
|
|
103
|
+
// ⚡ LAZY DOCS: Generate docs in background after startup to not block server
|
|
104
|
+
let docsCache = null;
|
|
105
|
+
let docsGenerating = false;
|
|
106
|
+
|
|
107
|
+
// Generate docs asynchronously after startup
|
|
108
|
+
const generateDocsInBackground = async () => {
|
|
109
|
+
if (docsCache || docsGenerating) return;
|
|
110
|
+
docsGenerating = true;
|
|
111
|
+
try {
|
|
112
|
+
console.log("Generating API documentation...");
|
|
113
|
+
docsCache = await docGen();
|
|
114
|
+
console.log("API documentation generated successfully");
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error("Error generating docs:", error);
|
|
117
|
+
docsCache = {
|
|
118
|
+
swagger: "2.0",
|
|
119
|
+
info: { title: "API Docs", version: "1.0.0" },
|
|
120
|
+
paths: {},
|
|
121
|
+
definitions: {}
|
|
122
|
+
};
|
|
123
|
+
} finally {
|
|
124
|
+
docsGenerating = false;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Start generating docs in background (non-blocking)
|
|
129
|
+
setTimeout(generateDocsInBackground, 100);
|
|
130
|
+
|
|
131
|
+
// Middleware to serve docs - will wait if still generating
|
|
132
|
+
app.use(async (ctx, next) => {
|
|
133
|
+
if (ctx.path === "/docs/spec" || ctx.path === "/docs/spec.json") {
|
|
134
|
+
// Wait for docs to be generated if not ready
|
|
135
|
+
while (docsGenerating && !docsCache) {
|
|
136
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
137
|
+
}
|
|
138
|
+
if (!docsCache) {
|
|
139
|
+
await generateDocsInBackground();
|
|
140
|
+
}
|
|
141
|
+
ctx.body = docsCache;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
await next();
|
|
145
|
+
});
|
|
146
|
+
|
|
99
147
|
app.use(
|
|
100
148
|
koaSwagger({
|
|
101
149
|
hideTopbar: true,
|
|
102
150
|
title: "Spice JS",
|
|
103
|
-
routePrefix: "/docs",
|
|
151
|
+
routePrefix: "/docs",
|
|
104
152
|
swaggerOptions: {
|
|
105
|
-
|
|
153
|
+
url: "/docs/spec", // Load spec from endpoint instead of inline
|
|
106
154
|
deepLinking: true,
|
|
107
155
|
displayRequestDuration: true,
|
|
108
156
|
jsonEditor: true,
|
|
109
157
|
},
|
|
110
|
-
exposeSpec:
|
|
158
|
+
exposeSpec: false, // We handle spec serving ourselves
|
|
111
159
|
})
|
|
112
160
|
);
|
|
113
|
-
await require("./loaders").load();
|
|
114
161
|
|
|
115
162
|
return spice;
|
|
116
163
|
} catch (e) {
|
package/src/models/SpiceModel.js
CHANGED
|
@@ -42,6 +42,10 @@ if (!Promise.allSettled) {
|
|
|
42
42
|
);
|
|
43
43
|
}
|
|
44
44
|
export default class SpiceModel {
|
|
45
|
+
// ⚡ Static caches for performance optimization
|
|
46
|
+
static _defaultsCache = {};
|
|
47
|
+
static _hiddenPropsCache = {};
|
|
48
|
+
|
|
45
49
|
constructor(args = {}) {
|
|
46
50
|
try {
|
|
47
51
|
var dbtype =
|
|
@@ -1257,6 +1261,46 @@ export default class SpiceModel {
|
|
|
1257
1261
|
return true;
|
|
1258
1262
|
}
|
|
1259
1263
|
|
|
1264
|
+
// ⚡ OPTIMIZED: Cache defaults metadata per model type
|
|
1265
|
+
getDefaultsMetadata(type) {
|
|
1266
|
+
const cacheKey = `${this.type}::${type}`;
|
|
1267
|
+
|
|
1268
|
+
if (!SpiceModel._defaultsCache[cacheKey]) {
|
|
1269
|
+
const staticDefaults = {};
|
|
1270
|
+
const dynamicDefaults = [];
|
|
1271
|
+
|
|
1272
|
+
// Pre-compute once per model type
|
|
1273
|
+
for (const key in this.props) {
|
|
1274
|
+
const def = this.props[key]?.defaults?.[type];
|
|
1275
|
+
if (def !== undefined) {
|
|
1276
|
+
if (_.isFunction(def)) {
|
|
1277
|
+
dynamicDefaults.push({ key, fn: def });
|
|
1278
|
+
} else {
|
|
1279
|
+
staticDefaults[key] = def;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
SpiceModel._defaultsCache[cacheKey] = {
|
|
1285
|
+
staticDefaults,
|
|
1286
|
+
dynamicDefaults,
|
|
1287
|
+
hasDefaults: Object.keys(staticDefaults).length > 0 || dynamicDefaults.length > 0
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
return SpiceModel._defaultsCache[cacheKey];
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// ⚡ OPTIMIZED: Cache hidden props per model type
|
|
1295
|
+
getHiddenProps() {
|
|
1296
|
+
if (!SpiceModel._hiddenPropsCache[this.type]) {
|
|
1297
|
+
SpiceModel._hiddenPropsCache[this.type] = Object.keys(this.props).filter(
|
|
1298
|
+
(key) => this.props[key]?.hide
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
return SpiceModel._hiddenPropsCache[this.type];
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1260
1304
|
async mapToObject(data, Class, source_property, store_property, property) {
|
|
1261
1305
|
let original_is_array = _.isArray(data);
|
|
1262
1306
|
if (!original_is_array) {
|
|
@@ -1486,26 +1530,29 @@ export default class SpiceModel {
|
|
|
1486
1530
|
data = [data];
|
|
1487
1531
|
}
|
|
1488
1532
|
|
|
1489
|
-
//
|
|
1490
|
-
const
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1533
|
+
// ⚡ OPTIMIZED: Use cached defaults metadata instead of computing every time
|
|
1534
|
+
const { staticDefaults, dynamicDefaults, hasDefaults } = this.getDefaultsMetadata(type);
|
|
1535
|
+
|
|
1536
|
+
if (hasDefaults) {
|
|
1537
|
+
data = data.map((item) => {
|
|
1538
|
+
// Apply static defaults first (fast - no function calls)
|
|
1539
|
+
const result = _.defaults({}, item, staticDefaults);
|
|
1540
|
+
|
|
1541
|
+
// Only compute dynamic defaults if there are any
|
|
1542
|
+
for (const { key, fn } of dynamicDefaults) {
|
|
1543
|
+
if (result[key] === undefined) {
|
|
1544
|
+
result[key] = fn({ old_data: data, new_data: old_data });
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
return result;
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1502
1551
|
|
|
1503
1552
|
// If type is "read", clean the data by omitting certain props.
|
|
1504
1553
|
if (type === "read") {
|
|
1505
|
-
//
|
|
1506
|
-
const hiddenProps =
|
|
1507
|
-
(key) => this.props[key]?.hide
|
|
1508
|
-
);
|
|
1554
|
+
// ⚡ OPTIMIZED: Use cached hidden props instead of computing every time
|
|
1555
|
+
const hiddenProps = this.getHiddenProps();
|
|
1509
1556
|
// Combine default props to remove.
|
|
1510
1557
|
const propsToClean = [
|
|
1511
1558
|
"deleted",
|