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 +366 -20
- package/lib/generators/controllers.js +79 -18
- package/lib/generators/crud.js +166 -65
- package/lib/generators/services.js +69 -10
- package/lib/index.js +15 -3
- package/lib/sourcemap.js +67 -0
- package/lib/utils.js +22 -4
- package/package.json +3 -3
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
|
|
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) =>
|
|
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 = `
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
-
|
|
630
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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 =
|
|
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) =>
|
|
49
|
-
|
|
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
|
|
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
|
-
|
|
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 };
|
package/lib/generators/crud.js
CHANGED
|
@@ -1,69 +1,159 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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) =>
|
|
29
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
fs.writeFileSync(path.join(outputDir, '
|
|
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
|
package/lib/sourcemap.js
ADDED
|
@@ -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
|
|
38
|
-
|
|
39
|
-
|
|
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.
|
|
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
|
-
"
|
|
25
|
-
"
|
|
24
|
+
"cron": "^4.4.0",
|
|
25
|
+
"knex-extended-crud": ">=2.0.31"
|
|
26
26
|
},
|
|
27
27
|
"peerDependenciesMeta": {
|
|
28
28
|
"cron": {
|