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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spice-js",
3
- "version": "2.6.70",
3
+ "version": "2.6.71",
4
4
  "description": "spice",
5
5
  "main": "build/index.js",
6
6
  "repository": {
@@ -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
- _.each(resources, async function (resource) {
6
+
7
+ for (const resource of resources) {
6
8
  let paths = path.join(spice.root_path, resource);
7
9
  spice[resource] = {};
8
- for (let file of require("fs").readdirSync(paths)) {
10
+
11
+ const files = require("fs").readdirSync(paths);
12
+
13
+ for (let file of files) {
9
14
  let file_name = file.split(".")[0];
10
- spice[resource][file_name.toLowerCase()] = (
11
- await import(path.join(paths, file))
12
- ).default;
13
- spice[resource][file_name] = (
14
- await import(path.join(paths, file))
15
- ).default;
16
- if (resource == "models") {
17
- new spice[resource][file_name]({});
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
  }
@@ -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
- let paths = path.join(spice.root_path, 'routes');
7
- require('fs').readdirSync(paths).forEach(function (file) {
8
- if (file != 'index.js') {
9
- let router = require(path.join(paths, file));
10
- spice.routers[file.split('.')[0]] = router;
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
- spice.app
13
- .use(spice.routers[file.split('.')[0]].routes())
14
- .use(spice.routers[file.split('.')[0]].allowedMethods());
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
- let paths = path.join(spice.root_path, "schema_extenders");
3
+ import { promisify } from "util";
4
+
5
+ const readdir = promisify(fs.readdir);
6
+
4
7
  spice.schema_extenders = {};
5
- if (fs.existsSync(paths)) {
6
- fs.readdirSync(paths).forEach(function (file) {
7
- if (path.extname(file) === ".js" && file !== "index.js") {
8
- try {
9
- let extender = require(path.join(paths, file));
10
- spice.schema_extenders[path.basename(file, ".js")] = extender;
11
- } catch (error) {
12
- console.error(`Failed to load ${file}:`, error);
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
- module.exports = function () {
6
- return new Promise(function (resolve, reject) {
7
- try {
8
- let locations = [spice.module_root_path + '/val', spice.root_path + '/validators']
9
-
10
- for (let i of locations) {
11
- let files = require('fs').readdirSync(i);
12
- files.forEach(function (file) {
13
- if (file != 'index.js') {
14
- file.split('.')[0];
15
- require(path.join(i, file));
16
- console.log('Loading Validator', path.join(i, file));
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", // host at /swagger instead of default /docs
151
+ routePrefix: "/docs",
104
152
  swaggerOptions: {
105
- spec: await docGen(),
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: true,
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) {
@@ -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
- // Compute the defaults from properties using reduce.
1490
- const defaults = Object.keys(this.props).reduce((acc, key) => {
1491
- const def = this.props[key]?.defaults?.[type];
1492
- if (def !== undefined) {
1493
- acc[key] = _.isFunction(def)
1494
- ? def({ old_data: data, new_data: old_data })
1495
- : def;
1496
- }
1497
- return acc;
1498
- }, {});
1499
-
1500
- // Merge defaults into each object.
1501
- data = data.map((item) => _.defaults(item, defaults));
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
- // Collect hidden properties from schema.
1506
- const hiddenProps = Object.keys(this.props).filter(
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",