langaro-api 1.0.5 → 1.0.7

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/lib/cli/init.js CHANGED
@@ -1,9 +1,24 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const readline = require('readline');
4
+ const crypto = require('crypto');
4
5
 
5
6
  const OWN_VERSION = require('../../package.json').version;
6
7
 
8
+ function generateUUID() {
9
+ return crypto.randomUUID();
10
+ }
11
+
12
+ function generateRandomString(length = 24) {
13
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
14
+ let result = '';
15
+ const bytes = crypto.randomBytes(length);
16
+ for (let i = 0; i < length; i++) {
17
+ result += chars[bytes[i] % chars.length];
18
+ }
19
+ return result;
20
+ }
21
+
7
22
  function ask(rl, question, defaultVal) {
8
23
  const suffix = defaultVal ? ` (${defaultVal})` : '';
9
24
  return new Promise((resolve) => {
@@ -199,11 +214,11 @@ newrelic_agent.log
199
214
  @types/generated/
200
215
  `;
201
216
 
202
- function envContent(port, dbName, redisPort) {
217
+ function envContent(port, dbName, redisPort, { jwtSecret = '', masterPass = '' } = {}) {
203
218
  return `NODE_ENV="development"
204
219
  TZ="UTC"
205
220
  PORT="${port}"
206
- APP_URL="http://localhost:${port}"
221
+ APP_URL="http://localhost:3000"
207
222
  API_URL="http://localhost:${port}"
208
223
 
209
224
  # Database
@@ -216,13 +231,13 @@ DB_PORT="3306"
216
231
  # Redis
217
232
  REDIS_HOST="127.0.0.1"
218
233
  REDIS_PORT="${redisPort}"
219
- REDIS_PASSWORD=""
234
+ # REDIS_PASSWORD=""
220
235
  # REDIS_USERNAME=""
221
236
  # REDIS_TLS=""
222
237
 
223
238
  # Security
224
- JWT_SECRET=""
225
- MASTER_PASS=""
239
+ JWT_SECRET="${jwtSecret}"
240
+ MASTER_PASS="${masterPass}"
226
241
 
227
242
  # Queue Management
228
243
  BULLBOARD_PATH="/admin/queues"
@@ -380,7 +395,23 @@ class App {
380
395
 
381
396
  if (process.env.NODE_ENV !== 'test') {
382
397
  this.express.use(morgan('dev', {
383
- skip: (req) => req.method === 'OPTIONS',
398
+ skip: (req) => {
399
+ // Ignore OPTIONS requests
400
+ if (req.method === 'OPTIONS') {
401
+ return true;
402
+ }
403
+
404
+ const skipUrls = [
405
+ process.env.BULLBOARD_PATH,
406
+ \`\${process.env.BULLBOARD_PATH}/static\`,
407
+ ];
408
+
409
+ if (skipUrls.indexOf(req.baseUrl) !== -1) {
410
+ return true;
411
+ }
412
+
413
+ return false;
414
+ },
384
415
  }));
385
416
  }
386
417
 
@@ -620,19 +651,16 @@ module.exports = function (services) {
620
651
  };
621
652
  `;
622
653
 
623
- const UTILS_INDEX = `const { ApiError } = require('trigger-api-error');
624
- const { StatusCodes } = require('http-status-codes');
625
- const { validateRequiredFields } = require('validate-required-fields');
626
- const { validateFieldsFormat } = require('validate-fields-format');
627
- const { fieldShouldNotBeSent } = require('forbid-fields-sent');
654
+ const UTILS_INDEX = `export * from 'validate-required-fields';
655
+ export * from 'trigger-api-error';
656
+ export * from 'http-status-codes';
657
+ export * from 'forbid-fields-sent';
658
+ export * from 'validate-fields-format';
659
+ export * from './sleep';
660
+ `;
628
661
 
629
- module.exports = {
630
- ApiError,
631
- StatusCodes,
632
- validateRequiredFields,
633
- validateFieldsFormat,
634
- fieldShouldNotBeSent,
635
- };
662
+ const UTILS_SLEEP = `// eslint-disable-next-line no-promise-executor-return
663
+ exports.sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
636
664
  `;
637
665
 
638
666
  const GLOBAL_SETUP_TESTS = `require('dotenv').config({ path: '.env.test' });
@@ -648,6 +676,319 @@ const matchers = require('jest-extended');
648
676
  expect.extend(matchers);
649
677
  `;
650
678
 
679
+ const QUEUES_JS = `require('dotenv').config({
680
+ path: process.env.NODE_ENV === 'test' ? '.env.test' : '.env',
681
+ });
682
+
683
+ const Sentry = require('@/config/instrument');
684
+ const redisConfig = require('@/database/redis-config');
685
+ const IORedis = require('ioredis');
686
+ const { Queue, Worker } = require('bullmq');
687
+ const { BullMQAdapter } = require('@bull-board/api/bullMQAdapter');
688
+ const { sleep } = require('@/utils');
689
+
690
+ const isTestEnv = process.env.NODE_ENV === 'test';
691
+ const inactivityCheckIntervalMs = 1 * 60 * 1000;
692
+ const inactivityTimeLimitMs = 10 * 60 * 1000;
693
+
694
+ let isShuttingDown = false;
695
+ let connection;
696
+ const jobConfigs = new Map();
697
+ const activeQueues = new Map();
698
+ let ioRef;
699
+ let bullBoardUpdateQueuesRef;
700
+ let cleanupIntervalId = null;
701
+
702
+ function createRedisConnection() {
703
+ const connectionInstance = new IORedis(redisConfig.connection);
704
+ connectionInstance.on('error', (error) => {
705
+ console.error('Redis Connection Error:', error);
706
+ Sentry.captureException(error, { tags: { service: 'redis' } });
707
+ });
708
+ return connectionInstance;
709
+ }
710
+
711
+ function getDefaultJobOptions() {
712
+ return {
713
+ attempts: 100,
714
+ backoff: { type: 'fixed', delay: 1000 * 60 },
715
+ removeOnComplete: { age: 60 * 60 * 24 * 30, count: 1000 },
716
+ removeOnFail: { age: 60 * 60 * 24 * 90, count: 5000 },
717
+ };
718
+ }
719
+
720
+ function getDefaultWorkerOptions() {
721
+ return {
722
+ autorun: false,
723
+ limiter: { max: 2, duration: 1000 },
724
+ maxStalledCount: 5,
725
+ };
726
+ }
727
+
728
+ function updateBullBoard() {
729
+ if (bullBoardUpdateQueuesRef && typeof bullBoardUpdateQueuesRef === 'function') {
730
+ bullBoardUpdateQueuesRef(Array.from(activeQueues.values()).map((qd) => new BullMQAdapter(qd.queue)));
731
+ }
732
+ }
733
+
734
+ async function getOrCreateQueueAndWorker(queueName, groupId, baseConfig) {
735
+ const queueNameWithGroupId = \`\${queueName}\${groupId ? \`-\${groupId}\` : ''}\`;
736
+
737
+ if (activeQueues.has(queueNameWithGroupId)) {
738
+ const existing = activeQueues.get(queueNameWithGroupId);
739
+ existing.lastActivity = Date.now();
740
+ return existing;
741
+ }
742
+
743
+ console.log(\`Creating queue and worker for: \${queueNameWithGroupId}\`);
744
+
745
+ const finalJobOptions = { ...getDefaultJobOptions(), ...(baseConfig.jobOptions || {}) };
746
+ const queue = new Queue(queueNameWithGroupId, { connection, defaultJobOptions: finalJobOptions });
747
+
748
+ const finalWorkerOptions = { connection, ...getDefaultWorkerOptions(), ...(baseConfig.workerOptions || {}) };
749
+ const worker = new Worker(
750
+ queueNameWithGroupId,
751
+ async (jobObj) => {
752
+ const queueData = activeQueues.get(queueNameWithGroupId);
753
+ if (queueData) { queueData.lastActivity = Date.now(); }
754
+ // eslint-disable-next-line no-use-before-define
755
+ await baseConfig.handle(jobObj.data, jobObj, { add }, ioRef);
756
+ },
757
+ finalWorkerOptions,
758
+ );
759
+
760
+ worker.on('failed', (job, error) => {
761
+ Sentry.captureException(error, {
762
+ tags: { queue: queueNameWithGroupId },
763
+ contexts: { data: { 'Job data': job?.data, queue: queueNameWithGroupId } },
764
+ });
765
+ });
766
+
767
+ worker.on('error', (error) => {
768
+ Sentry.captureException(error, { tags: { queue: queueNameWithGroupId, event: 'worker-error' } });
769
+ });
770
+
771
+ const queueData = {
772
+ queue, worker, lastActivity: Date.now(), name: queueNameWithGroupId, queueName, groupId,
773
+ };
774
+
775
+ if (!isShuttingDown) {
776
+ activeQueues.set(queueNameWithGroupId, queueData);
777
+ worker.run();
778
+ console.log(\`Worker running for: \${queueNameWithGroupId}\`);
779
+ }
780
+
781
+ updateBullBoard();
782
+ return queueData;
783
+ }
784
+
785
+ // eslint-disable-next-line no-unused-vars
786
+ async function add(queueName, data, options = {}) {
787
+ const baseConfig = jobConfigs.get(queueName);
788
+ if (!baseConfig) {
789
+ throw new Error(\`Queue configuration not found for: \${queueName}\`);
790
+ }
791
+
792
+ try {
793
+ if (baseConfig.group && !options.groupId) {
794
+ throw new Error(\`Missing groupId for grouped queue: \${queueName}\`);
795
+ }
796
+
797
+ const queueData = await getOrCreateQueueAndWorker(queueName, options.groupId, baseConfig);
798
+ await queueData.queue.add(queueData.name, data, options);
799
+ } catch (error) {
800
+ console.error(\`Failed to add job to queue \${queueName}:\`, error);
801
+ Sentry.captureException(error, { tags: { queue: queueName, operation: 'addJob' } });
802
+ throw error;
803
+ }
804
+ }
805
+
806
+ async function closeInactiveQueues() {
807
+ const now = Date.now();
808
+ const queuesToRemove = [];
809
+
810
+ const checks = Array.from(activeQueues.entries()).map(async ([queueName, queueData]) => {
811
+ if (now - queueData.lastActivity <= inactivityTimeLimitMs) return;
812
+ try {
813
+ const [activeCount, waitingCount, delayedCount] = await Promise.all([
814
+ queueData.queue.getActiveCount(),
815
+ queueData.queue.getWaitingCount(),
816
+ queueData.queue.getDelayedCount(),
817
+ ]);
818
+ if (activeCount === 0 && waitingCount === 0 && delayedCount === 0) {
819
+ queuesToRemove.push(queueName);
820
+ }
821
+ } catch (error) {
822
+ Sentry.captureException(error, { tags: { queue: queueName, operation: 'checkBeforeClose' } });
823
+ }
824
+ });
825
+
826
+ await Promise.allSettled(checks);
827
+ if (queuesToRemove.length === 0) return;
828
+
829
+ await Promise.allSettled(
830
+ queuesToRemove.map(async (queueName) => {
831
+ const queueData = activeQueues.get(queueName);
832
+ if (!queueData) return;
833
+ try {
834
+ await queueData.worker.close();
835
+ await queueData.queue.close();
836
+ activeQueues.delete(queueName);
837
+ } catch (error) {
838
+ Sentry.captureException(error, { tags: { queue: queueName, operation: 'closeInactive' } });
839
+ }
840
+ }),
841
+ );
842
+
843
+ updateBullBoard();
844
+ }
845
+
846
+ function startCleanupInterval() {
847
+ if (cleanupIntervalId) clearInterval(cleanupIntervalId);
848
+ if (!isTestEnv) {
849
+ cleanupIntervalId = setInterval(closeInactiveQueues, inactivityCheckIntervalMs);
850
+ }
851
+ }
852
+
853
+ async function startWorkersForExistingQueues() {
854
+ if (!connection) return;
855
+
856
+ const stream = connection.scanStream({ match: 'bull:*:meta', count: 1000 });
857
+ const potentialQueueNames = new Set();
858
+
859
+ stream.on('data', (keys) => {
860
+ (keys || []).forEach((key) => {
861
+ const match = key.match(/^bull:(.+):meta$/);
862
+ if (match && match[1]) potentialQueueNames.add(match[1]);
863
+ });
864
+ });
865
+
866
+ try {
867
+ await new Promise((resolve, reject) => { stream.on('end', resolve); stream.on('error', reject); });
868
+ } catch (error) {
869
+ Sentry.captureException(error, { tags: { operation: 'startWorkersForExistingQueues' } });
870
+ return;
871
+ }
872
+
873
+ if (potentialQueueNames.size === 0) return;
874
+
875
+ const sortedConfigKeys = Array.from(jobConfigs.keys()).sort((a, b) => b.length - a.length);
876
+
877
+ await Promise.all(
878
+ Array.from(potentialQueueNames).map(async (fullQueueName) => {
879
+ let baseName = null;
880
+ let groupId = null;
881
+ let config = null;
882
+
883
+ sortedConfigKeys.some((potentialBase) => {
884
+ const baseConfig = jobConfigs.get(potentialBase);
885
+ if (fullQueueName === potentialBase && !baseConfig.group) {
886
+ baseName = potentialBase;
887
+ config = baseConfig;
888
+ return true;
889
+ }
890
+ if (baseConfig.group && fullQueueName.startsWith(\`\${potentialBase}-\`)) {
891
+ baseName = potentialBase;
892
+ groupId = fullQueueName.slice(potentialBase.length + 1);
893
+ config = baseConfig;
894
+ return true;
895
+ }
896
+ return false;
897
+ });
898
+
899
+ if (!config || activeQueues.has(fullQueueName)) return;
900
+
901
+ try {
902
+ const activeCount = await connection.llen(\`bull:\${fullQueueName}:active\`);
903
+ const waitCount = await connection.llen(\`bull:\${fullQueueName}:wait\`);
904
+ const delayedCount = await connection.zcard(\`bull:\${fullQueueName}:delayed\`);
905
+
906
+ if (waitCount > 0 || delayedCount > 0 || activeCount > 0) {
907
+ await getOrCreateQueueAndWorker(baseName, groupId, config);
908
+ }
909
+ } catch (error) {
910
+ Sentry.captureException(error, { tags: { queue: fullQueueName, operation: 'startupScanInit' } });
911
+ }
912
+ }),
913
+ );
914
+ }
915
+
916
+ async function initializeQueues(services, socketIo, bullBoardReplaceQueues) {
917
+ if (isTestEnv) { return { add: jest.fn() }; }
918
+
919
+ console.log('Queue system initializing...');
920
+
921
+ connection = createRedisConnection();
922
+ ioRef = socketIo;
923
+ bullBoardUpdateQueuesRef = bullBoardReplaceQueues;
924
+
925
+ const { loadJobs } = require('langaro-api');
926
+ const jobs = loadJobs(services);
927
+ Object.entries(jobs).forEach(([name, job]) => {
928
+ jobConfigs.set(name, {
929
+ handle: job.handle,
930
+ jobOptions: job.jobOptions || {},
931
+ workerOptions: job.workerOptions || {},
932
+ group: job.group,
933
+ });
934
+ });
935
+
936
+ await startWorkersForExistingQueues();
937
+ startCleanupInterval();
938
+
939
+ console.log('Queue system initialized.');
940
+ return { add };
941
+ }
942
+
943
+ async function shutdownQueues() {
944
+ if (isShuttingDown) return;
945
+ isShuttingDown = true;
946
+
947
+ console.log('Gracefully shutting down queues...');
948
+
949
+ if (cleanupIntervalId) {
950
+ clearInterval(cleanupIntervalId);
951
+ cleanupIntervalId = null;
952
+ }
953
+
954
+ const activeQueuesValues = Array.from(activeQueues.values());
955
+
956
+ await Promise.allSettled(activeQueuesValues.map(async (qd) => {
957
+ try { await qd.worker.pause(); } catch (e) { console.error(\`Error pausing \${qd.name}:\`, e); }
958
+ }));
959
+
960
+ let retries = 0;
961
+ const maxRetries = 400;
962
+ while (retries++ < maxRetries) {
963
+ const stillActive = await Promise.all(activeQueuesValues.map((qd) => qd.queue.getActiveCount()));
964
+ if (stillActive.reduce((a, b) => a + b, 0) === 0) break;
965
+ await sleep(3000);
966
+ }
967
+
968
+ await Promise.allSettled(activeQueuesValues.map(async (qd) => {
969
+ try {
970
+ await qd.worker.close();
971
+ await qd.queue.close();
972
+ } catch (e) {
973
+ Sentry.captureException(e, { tags: { queue: qd.name, operation: 'shutdown' } });
974
+ }
975
+ }));
976
+
977
+ activeQueues.clear();
978
+ jobConfigs.clear();
979
+
980
+ if (connection) {
981
+ try { await connection.quit(); } catch (e) {
982
+ Sentry.captureException(e, { tags: { service: 'redis', operation: 'quit' } });
983
+ }
984
+ }
985
+
986
+ console.log('Queue shutdown complete.');
987
+ }
988
+
989
+ module.exports = { initializeQueues, shutdownQueues };
990
+ `;
991
+
651
992
  const MODELS_INDEX = `const { loadModels } = require('langaro-api');
652
993
 
653
994
  module.exports = (knexInstance) => loadModels(knexInstance, __dirname);
@@ -726,12 +1067,16 @@ async function run() {
726
1067
  writeFile(root, 'jest.config.js', JEST_CONFIG);
727
1068
  writeFile(root, '.gitignore', GITIGNORE);
728
1069
  writeFile(root, '.env.example', envContent(port, dbName, redisPort));
729
- writeFile(root, '.env', envContent(port, dbName, redisPort));
1070
+ writeFile(root, '.env', envContent(port, dbName, redisPort, {
1071
+ jwtSecret: generateUUID(),
1072
+ masterPass: generateRandomString(24),
1073
+ }));
730
1074
 
731
1075
  // src/config/
732
1076
  writeFile(root, 'src/config/server.js', serverJs(port));
733
1077
  writeFile(root, 'src/config/app.js', APP_JS);
734
1078
  writeFile(root, 'src/config/instrument.js', INSTRUMENT_JS);
1079
+ writeFile(root, 'src/config/queues.js', QUEUES_JS);
735
1080
  writeFile(root, 'src/config/tests/global-setup-tests.js', GLOBAL_SETUP_TESTS);
736
1081
  writeFile(root, 'src/config/tests/setup-tests.js', SETUP_TESTS);
737
1082
 
@@ -753,6 +1098,7 @@ async function run() {
753
1098
 
754
1099
  // src/utils
755
1100
  writeFile(root, 'src/utils/index.js', UTILS_INDEX);
1101
+ writeFile(root, 'src/utils/sleep.js', UTILS_SLEEP);
756
1102
 
757
1103
  // Empty directories
758
1104
  ['src/integrations', 'src/static', 'src/temp', 'src/database/tests'].forEach((dir) => {
@@ -762,7 +1108,7 @@ async function run() {
762
1108
  console.log(`\n\x1b[32mProject scaffolded!\x1b[0m\n`);
763
1109
  console.log('Next steps:');
764
1110
  console.log(` 1. \x1b[36mnpm install\x1b[0m`);
765
- console.log(` 2. Fill in your secrets in \x1b[36m.env\x1b[0m (JWT_SECRET, DB_PASSWORD, etc.)`);
1111
+ console.log(` 2. Fill in your \x1b[36m.env\x1b[0m (DB_PASSWORD, etc.)`);
766
1112
  console.log(` 3. Create the \x1b[36m${dbName}\x1b[0m database in MySQL`);
767
1113
  console.log(` 4. \x1b[36mnpm run dev\x1b[0m`);
768
1114
  console.log(` 5. \x1b[36mnpx langaro-api new\x1b[0m to create your first resource\n`);
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { pascalCase, extractMethods } = require('../utils');
3
+ const { pascalCase, extractMethodsWithLocations } = require('../utils');
4
+ const { generateSourceMap } = require('../sourcemap');
4
5
 
5
6
  function scanControllerEntries(controllersDir) {
6
7
  const entries = [];
@@ -22,42 +23,102 @@ function scanControllerEntries(controllersDir) {
22
23
  return entries.sort((a, b) => a.tableName.localeCompare(b.tableName));
23
24
  }
24
25
 
25
- function generateControllersDts(controllersDir) {
26
+ function resolveServiceFile(tableName, servicesDir, modelsDir) {
27
+ if (servicesDir) {
28
+ const serviceFile = path.join(servicesDir, `${tableName}.services.js`);
29
+ if (fs.existsSync(serviceFile)) return serviceFile;
30
+ }
31
+ if (modelsDir) {
32
+ const modelFile = path.join(modelsDir, `${tableName}.model.js`);
33
+ if (fs.existsSync(modelFile)) return modelFile;
34
+ }
35
+ return null;
36
+ }
37
+
38
+ function generateControllersDts(controllersDir, outputDir, servicesDir, modelsDir) {
26
39
  const lines = ['// Auto-generated by langaro-api — DO NOT EDIT', ''];
27
40
  const controllerEntries = scanControllerEntries(controllersDir);
41
+ const mappings = [];
42
+ const sources = [];
43
+ const sourceIndexMap = {};
44
+
45
+ function getSourceIndex(filePath) {
46
+ const rel = path.relative(outputDir, filePath).replace(/\\/g, '/');
47
+ if (!(rel in sourceIndexMap)) {
48
+ sourceIndexMap[rel] = sources.length;
49
+ sources.push(rel);
50
+ }
51
+ return sourceIndexMap[rel];
52
+ }
53
+
54
+ function addMapping(col, sourceIdx, srcLine, srcCol) {
55
+ mappings.push({
56
+ genLine: lines.length,
57
+ genCol: col,
58
+ sourceIndex: sourceIdx,
59
+ sourceLine: srcLine,
60
+ sourceCol: srcCol,
61
+ });
62
+ }
28
63
 
29
64
  // Base classes for JSDoc @param on "ServicesClass"
30
- controllerEntries.forEach(({ tableName }) => {
65
+ controllerEntries.forEach(({ tableName, filePath }) => {
31
66
  const pc = pascalCase(tableName);
32
- lines.push(
33
- `declare class ${pc}ControllerBase {`,
34
- ` service: ServicesMap['${pc}Services'];`,
35
- ' services: ServicesMap;',
36
- ' Queue: QueueMap;',
37
- ' io: any;',
38
- '}',
39
- '',
40
- );
67
+ const controllerIdx = getSourceIndex(filePath);
68
+ const serviceFile = resolveServiceFile(tableName, servicesDir, modelsDir);
69
+ const serviceIdx = serviceFile ? getSourceIndex(serviceFile) : controllerIdx;
70
+
71
+ addMapping(0, controllerIdx, 0, 0);
72
+ lines.push(`declare class ${pc}ControllerBase {`);
73
+
74
+ // service: property → maps to the service/model file
75
+ addMapping(0, serviceIdx, 0, 0);
76
+ lines.push(` service: ServicesMap['${pc}Services'];`);
77
+
78
+ // remaining properties → map to controller file
79
+ addMapping(0, controllerIdx, 0, 0);
80
+ lines.push(' services: ServicesMap;');
81
+ addMapping(0, controllerIdx, 0, 0);
82
+ lines.push(' Queue: QueueMap;');
83
+ addMapping(0, controllerIdx, 0, 0);
84
+ lines.push(' io: any;');
85
+ addMapping(0, controllerIdx, 0, 0);
86
+ lines.push('}');
87
+ lines.push('');
41
88
  });
42
89
 
43
90
  // Controller interfaces with their methods (for router autocomplete)
44
91
  controllerEntries.forEach(({ tableName, filePath }) => {
45
92
  const pc = pascalCase(tableName);
46
- const methods = extractMethods(filePath);
93
+ const methods = extractMethodsWithLocations(filePath);
94
+ const sourceIdx = getSourceIndex(filePath);
95
+
96
+ addMapping(0, sourceIdx, 0, 0);
47
97
  lines.push(`declare interface I${pc}Controller extends ${pc}ControllerBase {`);
48
- methods.forEach((m) => lines.push(` ${m}(...args: any[]): any;`));
49
- lines.push('}', '');
98
+ methods.forEach((m) => {
99
+ addMapping(2, sourceIdx, m.line, m.col);
100
+ lines.push(` ${m.name}(...args: any[]): any;`);
101
+ });
102
+ addMapping(0, sourceIdx, 0, 0);
103
+ lines.push('}');
104
+ lines.push('');
50
105
  });
51
106
 
52
- // ControllersMap — used to type the "controllers" param in routers
107
+ // ControllersMap
53
108
  lines.push('declare interface ControllersMap {');
54
- controllerEntries.forEach(({ tableName }) => {
109
+ controllerEntries.forEach(({ tableName, filePath }) => {
55
110
  const pc = pascalCase(tableName);
111
+ addMapping(2, getSourceIndex(filePath), 0, 0);
56
112
  lines.push(` ${pc}Controller: I${pc}Controller;`);
57
113
  });
58
114
  lines.push('}', '');
59
115
 
60
- return lines.join('\n');
116
+ lines.push('//# sourceMappingURL=controllers.d.ts.map');
117
+
118
+ const content = lines.join('\n');
119
+ const sourceMap = generateSourceMap('controllers.d.ts', sources, mappings);
120
+
121
+ return { content, sourceMap };
61
122
  }
62
123
 
63
124
  module.exports = { scanControllerEntries, generateControllersDts };
@@ -1,69 +1,159 @@
1
- module.exports = function generateCrudDts() {
2
- return [
1
+ const path = require('path');
2
+ const { extractMethodsWithLocations } = require('../utils');
3
+ const { generateSourceMap } = require('../sourcemap');
4
+
5
+ // Extract method name from a .d.ts declaration line like " getWhere(...): ..."
6
+ function extractMethodName(line) {
7
+ const match = line.match(/^\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/);
8
+ return match ? match[1] : null;
9
+ }
10
+
11
+ module.exports = function generateCrudDts(projectRoot, outputDir) {
12
+ // Try to resolve knex-extended-crud source
13
+ let crudSourcePath = null;
14
+ let methodLineMap = {};
15
+
16
+ if (projectRoot && outputDir) {
17
+ try {
18
+ crudSourcePath = require.resolve('knex-extended-crud', {
19
+ paths: [projectRoot],
20
+ });
21
+ const methods = extractMethodsWithLocations(crudSourcePath);
22
+ methods.forEach((m) => {
23
+ methodLineMap[m.name] = { line: m.line, col: m.col };
24
+ });
25
+ } catch {
26
+ crudSourcePath = null;
27
+ }
28
+ }
29
+
30
+ const hasMappings = !!crudSourcePath;
31
+ const mappings = [];
32
+ const sources = [];
33
+ let sourceIdx = -1;
34
+
35
+ if (hasMappings) {
36
+ const rel = path.relative(outputDir, crudSourcePath).replace(/\\/g, '/');
37
+ sources.push(rel);
38
+ sourceIdx = 0;
39
+ }
40
+
41
+ function addMethodMapping(linesArr, methodName) {
42
+ if (!hasMappings) return;
43
+ const loc = methodLineMap[methodName];
44
+ if (loc) {
45
+ mappings.push({
46
+ genLine: linesArr.length,
47
+ genCol: 2,
48
+ sourceIndex: sourceIdx,
49
+ sourceLine: loc.line,
50
+ sourceCol: loc.col,
51
+ });
52
+ }
53
+ }
54
+
55
+ const lines = [
3
56
  '// Auto-generated by langaro-api — DO NOT EDIT',
4
57
  '',
5
- 'declare class CRUD {',
6
- ' table: string;',
7
- ' knex: any;',
8
- ' hide: string[];',
9
- ' append: string[];',
10
- ' fields: Record<string, any>;',
11
- ' schema: any;',
12
- ' options: {',
13
- ' perPage: number;',
14
- ' currentPage: number;',
15
- ' isLengthAware: boolean;',
16
- ' exactMatch: boolean;',
17
- ' sortBy: string;',
18
- ' sort: string;',
19
- ' };',
20
- '',
21
- ' createTransaction(): Promise<any>;',
22
- '',
23
- ' create(data?: Record<string, any>, transaction?: any): Promise<{ success: boolean; data: any }>;',
24
- ' batchCreate(data?: any[], transaction?: any): Promise<{ success: boolean; data: any[] }>;',
25
- '',
26
- ' get(options?: CRUDGetOptions, transaction?: any): Promise<{ success: boolean; data: any[]; pagination?: any }>;',
27
- ' getWhere(prop: string, value: any, options?: CRUDGetOptions, transaction?: any): Promise<{ success: boolean; data: any }>;',
28
- ' search(field: string, term: string, options?: CRUDGetOptions, transaction?: any): Promise<{ success: boolean; data: any[] }>;',
29
- ' count(options?: CRUDGetOptions, transaction?: any): Promise<number>;',
30
- '',
31
- ' updateWhere(',
32
- ' prop: string, value: any, data?: Record<string, any>,',
33
- ' options?: CRUDUpdateOptions, transaction?: any',
34
- ' ): Promise<{ success: boolean; data: any }>;',
35
- ' batchUpdate(data: any[], options?: CRUDBatchUpdateOptions, transaction?: any): Promise<any[]>;',
36
- '',
37
- ' deleteWhere(',
38
- ' prop: string, values: any, options?: CRUDDeleteOptions, transaction?: any',
39
- ' ): Promise<{ success: boolean; data: any }>;',
40
- '',
41
- ' defaultInsertValidations(',
42
- ' requestData: Record<string, any>, options?: Record<string, any>, transaction?: any',
43
- ' ): Promise<Record<string, any>>;',
44
- ' defaultUpdateValidations(',
45
- ' itemId: any, requestData: Record<string, any>,',
46
- ' options?: Record<string, any>, transaction?: any',
47
- ' ): Promise<Record<string, any>>;',
48
- ' validateAndFormatFields(',
49
- ' data: Record<string, any>, skipFields?: string[], transaction?: any',
50
- ' ): Promise<{ success: boolean; data?: Record<string, any>; err?: string }>;',
51
- ' validateForeignIds(data: Record<string, any>, transaction?: any): Promise<void>;',
52
- '',
53
- ' tableInfo(options?: Record<string, any>, transaction?: any): Promise<Record<string, any>>;',
54
- ' requiredFields(options?: Record<string, any>, transaction?: any): Promise<{ data: string[] }>;',
55
- '',
56
- ' appendItems(',
57
- ' items: any[], appendArr: string[], appendOptions?: Record<string, any>, transaction?: any',
58
- ' ): Promise<any[]>;',
59
- ' refreshCache(transaction?: any): Promise<void>;',
60
- ' clearSchemaCache(): void;',
61
- ' clearTableCache(tableName: string): void;',
62
- ' disableCache(): void;',
63
- ' enableCache(): void;',
64
- ' setCacheTimeout(timeout: number): void;',
65
- '}',
66
- '',
58
+ ];
59
+
60
+ // CRUD class
61
+ lines.push('declare class CRUD {');
62
+ lines.push(' table: string;');
63
+ lines.push(' knex: any;');
64
+ lines.push(' hide: string[];');
65
+ lines.push(' append: string[];');
66
+ lines.push(' fields: Record<string, any>;');
67
+ lines.push(' schema: any;');
68
+ lines.push(' options: {');
69
+ lines.push(' perPage: number;');
70
+ lines.push(' currentPage: number;');
71
+ lines.push(' isLengthAware: boolean;');
72
+ lines.push(' exactMatch: boolean;');
73
+ lines.push(' sortBy: string;');
74
+ lines.push(' sort: string;');
75
+ lines.push(' };');
76
+ lines.push('');
77
+
78
+ addMethodMapping(lines, 'createTransaction');
79
+ lines.push(' createTransaction(): Promise<any>;');
80
+ lines.push('');
81
+
82
+ addMethodMapping(lines, 'create');
83
+ lines.push(' create(data?: Record<string, any>, transaction?: any): Promise<{ success: boolean; data: any }>;');
84
+ addMethodMapping(lines, 'batchCreate');
85
+ lines.push(' batchCreate(data?: any[], transaction?: any): Promise<{ success: boolean; data: any[] }>;');
86
+ lines.push('');
87
+
88
+ addMethodMapping(lines, 'get');
89
+ lines.push(' get(options?: CRUDGetOptions, transaction?: any): Promise<{ success: boolean; data: any[]; pagination?: any }>;');
90
+ addMethodMapping(lines, 'getWhere');
91
+ lines.push(' getWhere(prop: string, value: any, options?: CRUDGetOptions, transaction?: any): Promise<{ success: boolean; data: any }>;');
92
+ addMethodMapping(lines, 'search');
93
+ lines.push(' search(field: string, term: string, options?: CRUDGetOptions, transaction?: any): Promise<{ success: boolean; data: any[] }>;');
94
+ addMethodMapping(lines, 'count');
95
+ lines.push(' count(options?: CRUDGetOptions, transaction?: any): Promise<number>;');
96
+ lines.push('');
97
+
98
+ addMethodMapping(lines, 'updateWhere');
99
+ lines.push(' updateWhere(');
100
+ lines.push(' prop: string, value: any, data?: Record<string, any>,');
101
+ lines.push(' options?: CRUDUpdateOptions, transaction?: any');
102
+ lines.push(' ): Promise<{ success: boolean; data: any }>;');
103
+ addMethodMapping(lines, 'batchUpdate');
104
+ lines.push(' batchUpdate(data: any[], options?: CRUDBatchUpdateOptions, transaction?: any): Promise<any[]>;');
105
+ lines.push('');
106
+
107
+ addMethodMapping(lines, 'deleteWhere');
108
+ lines.push(' deleteWhere(');
109
+ lines.push(' prop: string, values: any, options?: CRUDDeleteOptions, transaction?: any');
110
+ lines.push(' ): Promise<{ success: boolean; data: any }>;');
111
+ lines.push('');
112
+
113
+ addMethodMapping(lines, 'defaultInsertValidations');
114
+ lines.push(' defaultInsertValidations(');
115
+ lines.push(' requestData: Record<string, any>, options?: Record<string, any>, transaction?: any');
116
+ lines.push(' ): Promise<Record<string, any>>;');
117
+ addMethodMapping(lines, 'defaultUpdateValidations');
118
+ lines.push(' defaultUpdateValidations(');
119
+ lines.push(' itemId: any, requestData: Record<string, any>,');
120
+ lines.push(' options?: Record<string, any>, transaction?: any');
121
+ lines.push(' ): Promise<Record<string, any>>;');
122
+ addMethodMapping(lines, 'validateAndFormatFields');
123
+ lines.push(' validateAndFormatFields(');
124
+ lines.push(' data: Record<string, any>, skipFields?: string[], transaction?: any');
125
+ lines.push(' ): Promise<{ success: boolean; data?: Record<string, any>; err?: string }>;');
126
+ addMethodMapping(lines, 'validateForeignIds');
127
+ lines.push(' validateForeignIds(data: Record<string, any>, transaction?: any): Promise<void>;');
128
+ lines.push('');
129
+
130
+ addMethodMapping(lines, 'tableInfo');
131
+ lines.push(' tableInfo(options?: Record<string, any>, transaction?: any): Promise<Record<string, any>>;');
132
+ addMethodMapping(lines, 'requiredFields');
133
+ lines.push(' requiredFields(options?: Record<string, any>, transaction?: any): Promise<{ data: string[] }>;');
134
+ lines.push('');
135
+
136
+ addMethodMapping(lines, 'appendItems');
137
+ lines.push(' appendItems(');
138
+ lines.push(' items: any[], appendArr: string[], appendOptions?: Record<string, any>, transaction?: any');
139
+ lines.push(' ): Promise<any[]>;');
140
+ addMethodMapping(lines, 'refreshCache');
141
+ lines.push(' refreshCache(transaction?: any): Promise<void>;');
142
+ addMethodMapping(lines, 'clearSchemaCache');
143
+ lines.push(' clearSchemaCache(): void;');
144
+ addMethodMapping(lines, 'clearTableCache');
145
+ lines.push(' clearTableCache(tableName: string): void;');
146
+ addMethodMapping(lines, 'disableCache');
147
+ lines.push(' disableCache(): void;');
148
+ addMethodMapping(lines, 'enableCache');
149
+ lines.push(' enableCache(): void;');
150
+ addMethodMapping(lines, 'setCacheTimeout');
151
+ lines.push(' setCacheTimeout(timeout: number): void;');
152
+ lines.push('}');
153
+ lines.push('');
154
+
155
+ // Options interfaces (no source mappings needed)
156
+ lines.push(
67
157
  'declare interface CRUDGetOptions {',
68
158
  ' perPage?: number;',
69
159
  ' currentPage?: number;',
@@ -153,5 +243,16 @@ module.exports = function generateCrudDts() {
153
243
  ' };',
154
244
  '}',
155
245
  '',
156
- ].join('\n');
246
+ );
247
+
248
+ if (hasMappings) {
249
+ lines.push('//# sourceMappingURL=crud.d.ts.map');
250
+ }
251
+
252
+ const content = lines.join('\n');
253
+ const sourceMap = hasMappings
254
+ ? generateSourceMap('crud.d.ts', sources, mappings)
255
+ : null;
256
+
257
+ return { content, sourceMap };
157
258
  };
@@ -1,10 +1,23 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { pascalCase, extractMethods, isCustomService } = require('../utils');
3
+ const { pascalCase, extractMethodsWithLocations, isCustomService } = require('../utils');
4
+ const { generateSourceMap } = require('../sourcemap');
4
5
 
5
- module.exports = function generateServicesDts(servicesDir, modelsDir) {
6
+ module.exports = function generateServicesDts(servicesDir, modelsDir, outputDir) {
6
7
  const lines = ['// Auto-generated by langaro-api — DO NOT EDIT', ''];
7
8
  const serviceEntries = [];
9
+ const mappings = [];
10
+ const sources = [];
11
+ const sourceIndexMap = {};
12
+
13
+ function getSourceIndex(filePath) {
14
+ const rel = path.relative(outputDir, filePath).replace(/\\/g, '/');
15
+ if (!(rel in sourceIndexMap)) {
16
+ sourceIndexMap[rel] = sources.length;
17
+ sources.push(rel);
18
+ }
19
+ return sourceIndexMap[rel];
20
+ }
8
21
 
9
22
  // Services with custom .services.js files
10
23
  if (fs.existsSync(servicesDir)) {
@@ -17,22 +30,42 @@ module.exports = function generateServicesDts(servicesDir, modelsDir) {
17
30
  const interfaceName = `I${serviceName}`;
18
31
  const filePath = path.join(servicesDir, file);
19
32
  const custom = isCustomService(filePath);
20
- const methods = extractMethods(filePath);
33
+ const methods = extractMethodsWithLocations(filePath);
34
+ const sourceIdx = getSourceIndex(filePath);
35
+
36
+ const addMapping = (col, srcLine, srcCol) => {
37
+ mappings.push({
38
+ genLine: lines.length,
39
+ genCol: col,
40
+ sourceIndex: sourceIdx,
41
+ sourceLine: srcLine,
42
+ sourceCol: srcCol,
43
+ });
44
+ };
21
45
 
22
46
  if (custom) {
47
+ addMapping(0, 0, 0);
23
48
  lines.push(`declare interface ${interfaceName} {`);
49
+ addMapping(0, 0, 0);
24
50
  lines.push(' table: string;');
25
51
  } else {
52
+ addMapping(0, 0, 0);
26
53
  lines.push(`declare interface ${interfaceName} extends CRUD {`);
27
54
  }
28
- methods.forEach((m) => lines.push(` ${m}(...args: any[]): any;`));
29
- lines.push('}', '');
55
+ methods.forEach((m) => {
56
+ addMapping(2, m.line, m.col);
57
+ lines.push(` ${m.name}(...args: any[]): any;`);
58
+ });
59
+ addMapping(0, 0, 0);
60
+ lines.push('}');
61
+ lines.push('');
30
62
 
31
63
  serviceEntries.push({
32
64
  interfaceName,
33
65
  serviceName,
34
66
  tableName,
35
67
  custom,
68
+ sourceIdx,
36
69
  });
37
70
  });
38
71
  }
@@ -60,12 +93,22 @@ module.exports = function generateServicesDts(servicesDir, modelsDir) {
60
93
  lines.push('declare interface ServicesMap {');
61
94
  serviceEntries
62
95
  .sort((a, b) => a.serviceName.localeCompare(b.serviceName))
63
- .forEach(({ interfaceName, serviceName }) => {
96
+ .forEach(({ interfaceName, serviceName, sourceIdx }) => {
97
+ const genLine = lines.length;
64
98
  lines.push(` ${serviceName}: ${interfaceName};`);
99
+ if (sourceIdx !== undefined) {
100
+ mappings.push({
101
+ genLine,
102
+ genCol: 2,
103
+ sourceIndex: sourceIdx,
104
+ sourceLine: 0,
105
+ sourceCol: 0,
106
+ });
107
+ }
65
108
  });
66
109
  lines.push('}', '');
67
110
 
68
- // ModelsConstructorMap — typed table names for "new models.xxx()"
111
+ // ModelsConstructorMap
69
112
  lines.push('declare interface ModelsConstructorMap {');
70
113
  serviceEntries
71
114
  .filter(({ custom }) => !custom)
@@ -77,10 +120,26 @@ module.exports = function generateServicesDts(servicesDir, modelsDir) {
77
120
  lines.push('}', '');
78
121
 
79
122
  // Base classes for JSDoc @param on the "model" parameter
80
- serviceEntries.forEach(({ serviceName, custom }) => {
81
- if (!custom) lines.push(`declare class ${serviceName}Base extends CRUD {}`);
123
+ serviceEntries.forEach(({ serviceName, custom, sourceIdx }) => {
124
+ if (!custom) {
125
+ if (sourceIdx !== undefined) {
126
+ mappings.push({
127
+ genLine: lines.length,
128
+ genCol: 0,
129
+ sourceIndex: sourceIdx,
130
+ sourceLine: 0,
131
+ sourceCol: 0,
132
+ });
133
+ }
134
+ lines.push(`declare class ${serviceName}Base extends CRUD {}`);
135
+ }
82
136
  });
83
137
  lines.push('');
84
138
 
85
- return lines.join('\n');
139
+ lines.push('//# sourceMappingURL=services.d.ts.map');
140
+
141
+ const content = lines.join('\n');
142
+ const sourceMap = generateSourceMap('services.d.ts', sources, mappings);
143
+
144
+ return { content, sourceMap };
86
145
  };
package/lib/index.js CHANGED
@@ -44,9 +44,21 @@ function generateTypes(userConfig = {}) {
44
44
 
45
45
  // Generate .d.ts files
46
46
  fs.mkdirSync(outputDir, { recursive: true });
47
- fs.writeFileSync(path.join(outputDir, 'crud.d.ts'), generateCrudDts());
48
- fs.writeFileSync(path.join(outputDir, 'services.d.ts'), generateServicesDts(servicesDir, modelsDir));
49
- fs.writeFileSync(path.join(outputDir, 'controllers.d.ts'), generateControllersDts(controllersDir));
47
+
48
+ const crud = generateCrudDts(root, outputDir);
49
+ fs.writeFileSync(path.join(outputDir, 'crud.d.ts'), crud.content);
50
+ if (crud.sourceMap) {
51
+ fs.writeFileSync(path.join(outputDir, 'crud.d.ts.map'), crud.sourceMap);
52
+ }
53
+
54
+ const services = generateServicesDts(servicesDir, modelsDir, outputDir);
55
+ fs.writeFileSync(path.join(outputDir, 'services.d.ts'), services.content);
56
+ fs.writeFileSync(path.join(outputDir, 'services.d.ts.map'), services.sourceMap);
57
+
58
+ const controllers = generateControllersDts(controllersDir, outputDir, servicesDir, modelsDir);
59
+ fs.writeFileSync(path.join(outputDir, 'controllers.d.ts'), controllers.content);
60
+ fs.writeFileSync(path.join(outputDir, 'controllers.d.ts.map'), controllers.sourceMap);
61
+
50
62
  fs.writeFileSync(path.join(outputDir, 'index.d.ts'), generateIndexDts());
51
63
 
52
64
  // Inject JSDoc annotations into source files
@@ -0,0 +1,67 @@
1
+ const BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
2
+
3
+ function encodeVLQ(value) {
4
+ let vlq = value < 0 ? (-value << 1) + 1 : value << 1;
5
+ let encoded = '';
6
+ do {
7
+ let digit = vlq & 0x1f;
8
+ vlq >>>= 5;
9
+ if (vlq > 0) digit |= 0x20;
10
+ encoded += BASE64[digit];
11
+ } while (vlq > 0);
12
+ return encoded;
13
+ }
14
+
15
+ /**
16
+ * Generate a source map JSON string.
17
+ *
18
+ * @param {string} generatedFile - The name of the generated .d.ts file
19
+ * @param {string[]} sources - Array of relative source file paths
20
+ * @param {Array<{genLine: number, genCol: number, sourceIndex: number, sourceLine: number, sourceCol: number}>} mappings
21
+ * @returns {string} JSON source map
22
+ */
23
+ function generateSourceMap(generatedFile, sources, mappings) {
24
+ const sorted = [...mappings].sort((a, b) => a.genLine - b.genLine || a.genCol - b.genCol);
25
+
26
+ let prevGenLine = 0;
27
+ let prevSourceIndex = 0;
28
+ let prevSourceLine = 0;
29
+ let prevSourceCol = 0;
30
+
31
+ const lineSegments = [];
32
+
33
+ sorted.forEach((m) => {
34
+ // Fill empty lines with ;
35
+ while (prevGenLine < m.genLine) {
36
+ lineSegments.push('');
37
+ prevGenLine++;
38
+ }
39
+
40
+ const segment =
41
+ encodeVLQ(m.genCol) +
42
+ encodeVLQ(m.sourceIndex - prevSourceIndex) +
43
+ encodeVLQ(m.sourceLine - prevSourceLine) +
44
+ encodeVLQ(m.sourceCol - prevSourceCol);
45
+
46
+ // Append to current line
47
+ if (lineSegments.length === 0 || prevGenLine > lineSegments.length - 1) {
48
+ lineSegments.push(segment);
49
+ } else {
50
+ const existing = lineSegments[lineSegments.length - 1];
51
+ lineSegments[lineSegments.length - 1] = existing ? existing + ',' + segment : segment;
52
+ }
53
+
54
+ prevSourceIndex = m.sourceIndex;
55
+ prevSourceLine = m.sourceLine;
56
+ prevSourceCol = m.sourceCol;
57
+ });
58
+
59
+ return JSON.stringify({
60
+ version: 3,
61
+ file: generatedFile,
62
+ sources,
63
+ mappings: lineSegments.join(';'),
64
+ });
65
+ }
66
+
67
+ module.exports = { generateSourceMap };
package/lib/utils.js CHANGED
@@ -26,6 +26,10 @@ function matchAll(regex, text) {
26
26
  }
27
27
 
28
28
  function extractMethods(filePath) {
29
+ return extractMethodsWithLocations(filePath).map((m) => m.name);
30
+ }
31
+
32
+ function extractMethodsWithLocations(filePath) {
29
33
  const content = fs.readFileSync(filePath, 'utf8');
30
34
 
31
35
  const patterns = [
@@ -34,10 +38,23 @@ function extractMethods(filePath) {
34
38
  /^async\s+function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm,
35
39
  ];
36
40
 
37
- const names = patterns.flatMap((r) => matchAll(r, content).map((m) => m[1]));
38
- return [...new Set(names)].filter(
39
- (n) => n !== 'constructor' && !n.startsWith('_') && !JS_RESERVED.has(n),
40
- );
41
+ const seen = new Set();
42
+ const results = [];
43
+
44
+ patterns.forEach((r) => {
45
+ matchAll(r, content).forEach((m) => {
46
+ const name = m[1];
47
+ if (name === 'constructor' || name.startsWith('_') || JS_RESERVED.has(name)) return;
48
+ if (seen.has(name)) return;
49
+ seen.add(name);
50
+ const line = content.substring(0, m.index).split('\n').length - 1;
51
+ const lineText = content.split('\n')[line];
52
+ const col = lineText.indexOf(name);
53
+ results.push({ name, line, col: col >= 0 ? col : 0 });
54
+ });
55
+ });
56
+
57
+ return results;
41
58
  }
42
59
 
43
60
  function isCustomService(filePath) {
@@ -64,6 +81,7 @@ module.exports = {
64
81
  pascalCase,
65
82
  matchAll,
66
83
  extractMethods,
84
+ extractMethodsWithLocations,
67
85
  isCustomService,
68
86
  scanJsFilesRecursively,
69
87
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "langaro-api",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Auto-generate TypeScript types, JSDoc annotations, and boilerplate loaders for knex-extended-crud projects",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -21,8 +21,8 @@
21
21
  "boilerplate"
22
22
  ],
23
23
  "peerDependencies": {
24
- "knex-extended-crud": ">=2.0.31",
25
- "cron": ">=3.0.0"
24
+ "cron": "^4.4.0",
25
+ "knex-extended-crud": ">=2.0.31"
26
26
  },
27
27
  "peerDependenciesMeta": {
28
28
  "cron": {