langaro-api 1.0.4 → 1.0.6
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 +349 -19
- package/package.json +2 -2
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"
|
|
@@ -620,19 +635,16 @@ module.exports = function (services) {
|
|
|
620
635
|
};
|
|
621
636
|
`;
|
|
622
637
|
|
|
623
|
-
const UTILS_INDEX = `
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
638
|
+
const UTILS_INDEX = `export * from 'validate-required-fields';
|
|
639
|
+
export * from 'trigger-api-error';
|
|
640
|
+
export * from 'http-status-codes';
|
|
641
|
+
export * from 'forbid-fields-sent';
|
|
642
|
+
export * from 'validate-fields-format';
|
|
643
|
+
export * from './sleep';
|
|
644
|
+
`;
|
|
628
645
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
StatusCodes,
|
|
632
|
-
validateRequiredFields,
|
|
633
|
-
validateFieldsFormat,
|
|
634
|
-
fieldShouldNotBeSent,
|
|
635
|
-
};
|
|
646
|
+
const UTILS_SLEEP = `// eslint-disable-next-line no-promise-executor-return
|
|
647
|
+
exports.sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
636
648
|
`;
|
|
637
649
|
|
|
638
650
|
const GLOBAL_SETUP_TESTS = `require('dotenv').config({ path: '.env.test' });
|
|
@@ -648,6 +660,319 @@ const matchers = require('jest-extended');
|
|
|
648
660
|
expect.extend(matchers);
|
|
649
661
|
`;
|
|
650
662
|
|
|
663
|
+
const QUEUES_JS = `require('dotenv').config({
|
|
664
|
+
path: process.env.NODE_ENV === 'test' ? '.env.test' : '.env',
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
const Sentry = require('@/config/instrument');
|
|
668
|
+
const redisConfig = require('@/database/redis-config');
|
|
669
|
+
const IORedis = require('ioredis');
|
|
670
|
+
const { Queue, Worker } = require('bullmq');
|
|
671
|
+
const { BullMQAdapter } = require('@bull-board/api/bullMQAdapter');
|
|
672
|
+
const { sleep } = require('@/utils');
|
|
673
|
+
|
|
674
|
+
const isTestEnv = process.env.NODE_ENV === 'test';
|
|
675
|
+
const inactivityCheckIntervalMs = 1 * 60 * 1000;
|
|
676
|
+
const inactivityTimeLimitMs = 10 * 60 * 1000;
|
|
677
|
+
|
|
678
|
+
let isShuttingDown = false;
|
|
679
|
+
let connection;
|
|
680
|
+
const jobConfigs = new Map();
|
|
681
|
+
const activeQueues = new Map();
|
|
682
|
+
let ioRef;
|
|
683
|
+
let bullBoardUpdateQueuesRef;
|
|
684
|
+
let cleanupIntervalId = null;
|
|
685
|
+
|
|
686
|
+
function createRedisConnection() {
|
|
687
|
+
const connectionInstance = new IORedis(redisConfig.connection);
|
|
688
|
+
connectionInstance.on('error', (error) => {
|
|
689
|
+
console.error('Redis Connection Error:', error);
|
|
690
|
+
Sentry.captureException(error, { tags: { service: 'redis' } });
|
|
691
|
+
});
|
|
692
|
+
return connectionInstance;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function getDefaultJobOptions() {
|
|
696
|
+
return {
|
|
697
|
+
attempts: 100,
|
|
698
|
+
backoff: { type: 'fixed', delay: 1000 * 60 },
|
|
699
|
+
removeOnComplete: { age: 60 * 60 * 24 * 30, count: 1000 },
|
|
700
|
+
removeOnFail: { age: 60 * 60 * 24 * 90, count: 5000 },
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function getDefaultWorkerOptions() {
|
|
705
|
+
return {
|
|
706
|
+
autorun: false,
|
|
707
|
+
limiter: { max: 2, duration: 1000 },
|
|
708
|
+
maxStalledCount: 5,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function updateBullBoard() {
|
|
713
|
+
if (bullBoardUpdateQueuesRef && typeof bullBoardUpdateQueuesRef === 'function') {
|
|
714
|
+
bullBoardUpdateQueuesRef(Array.from(activeQueues.values()).map((qd) => new BullMQAdapter(qd.queue)));
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
async function getOrCreateQueueAndWorker(queueName, groupId, baseConfig) {
|
|
719
|
+
const queueNameWithGroupId = \`\${queueName}\${groupId ? \`-\${groupId}\` : ''}\`;
|
|
720
|
+
|
|
721
|
+
if (activeQueues.has(queueNameWithGroupId)) {
|
|
722
|
+
const existing = activeQueues.get(queueNameWithGroupId);
|
|
723
|
+
existing.lastActivity = Date.now();
|
|
724
|
+
return existing;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
console.log(\`Creating queue and worker for: \${queueNameWithGroupId}\`);
|
|
728
|
+
|
|
729
|
+
const finalJobOptions = { ...getDefaultJobOptions(), ...(baseConfig.jobOptions || {}) };
|
|
730
|
+
const queue = new Queue(queueNameWithGroupId, { connection, defaultJobOptions: finalJobOptions });
|
|
731
|
+
|
|
732
|
+
const finalWorkerOptions = { connection, ...getDefaultWorkerOptions(), ...(baseConfig.workerOptions || {}) };
|
|
733
|
+
const worker = new Worker(
|
|
734
|
+
queueNameWithGroupId,
|
|
735
|
+
async (jobObj) => {
|
|
736
|
+
const queueData = activeQueues.get(queueNameWithGroupId);
|
|
737
|
+
if (queueData) { queueData.lastActivity = Date.now(); }
|
|
738
|
+
// eslint-disable-next-line no-use-before-define
|
|
739
|
+
await baseConfig.handle(jobObj.data, jobObj, { add }, ioRef);
|
|
740
|
+
},
|
|
741
|
+
finalWorkerOptions,
|
|
742
|
+
);
|
|
743
|
+
|
|
744
|
+
worker.on('failed', (job, error) => {
|
|
745
|
+
Sentry.captureException(error, {
|
|
746
|
+
tags: { queue: queueNameWithGroupId },
|
|
747
|
+
contexts: { data: { 'Job data': job?.data, queue: queueNameWithGroupId } },
|
|
748
|
+
});
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
worker.on('error', (error) => {
|
|
752
|
+
Sentry.captureException(error, { tags: { queue: queueNameWithGroupId, event: 'worker-error' } });
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
const queueData = {
|
|
756
|
+
queue, worker, lastActivity: Date.now(), name: queueNameWithGroupId, queueName, groupId,
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
if (!isShuttingDown) {
|
|
760
|
+
activeQueues.set(queueNameWithGroupId, queueData);
|
|
761
|
+
worker.run();
|
|
762
|
+
console.log(\`Worker running for: \${queueNameWithGroupId}\`);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
updateBullBoard();
|
|
766
|
+
return queueData;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// eslint-disable-next-line no-unused-vars
|
|
770
|
+
async function add(queueName, data, options = {}) {
|
|
771
|
+
const baseConfig = jobConfigs.get(queueName);
|
|
772
|
+
if (!baseConfig) {
|
|
773
|
+
throw new Error(\`Queue configuration not found for: \${queueName}\`);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
try {
|
|
777
|
+
if (baseConfig.group && !options.groupId) {
|
|
778
|
+
throw new Error(\`Missing groupId for grouped queue: \${queueName}\`);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const queueData = await getOrCreateQueueAndWorker(queueName, options.groupId, baseConfig);
|
|
782
|
+
await queueData.queue.add(queueData.name, data, options);
|
|
783
|
+
} catch (error) {
|
|
784
|
+
console.error(\`Failed to add job to queue \${queueName}:\`, error);
|
|
785
|
+
Sentry.captureException(error, { tags: { queue: queueName, operation: 'addJob' } });
|
|
786
|
+
throw error;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
async function closeInactiveQueues() {
|
|
791
|
+
const now = Date.now();
|
|
792
|
+
const queuesToRemove = [];
|
|
793
|
+
|
|
794
|
+
const checks = Array.from(activeQueues.entries()).map(async ([queueName, queueData]) => {
|
|
795
|
+
if (now - queueData.lastActivity <= inactivityTimeLimitMs) return;
|
|
796
|
+
try {
|
|
797
|
+
const [activeCount, waitingCount, delayedCount] = await Promise.all([
|
|
798
|
+
queueData.queue.getActiveCount(),
|
|
799
|
+
queueData.queue.getWaitingCount(),
|
|
800
|
+
queueData.queue.getDelayedCount(),
|
|
801
|
+
]);
|
|
802
|
+
if (activeCount === 0 && waitingCount === 0 && delayedCount === 0) {
|
|
803
|
+
queuesToRemove.push(queueName);
|
|
804
|
+
}
|
|
805
|
+
} catch (error) {
|
|
806
|
+
Sentry.captureException(error, { tags: { queue: queueName, operation: 'checkBeforeClose' } });
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
await Promise.allSettled(checks);
|
|
811
|
+
if (queuesToRemove.length === 0) return;
|
|
812
|
+
|
|
813
|
+
await Promise.allSettled(
|
|
814
|
+
queuesToRemove.map(async (queueName) => {
|
|
815
|
+
const queueData = activeQueues.get(queueName);
|
|
816
|
+
if (!queueData) return;
|
|
817
|
+
try {
|
|
818
|
+
await queueData.worker.close();
|
|
819
|
+
await queueData.queue.close();
|
|
820
|
+
activeQueues.delete(queueName);
|
|
821
|
+
} catch (error) {
|
|
822
|
+
Sentry.captureException(error, { tags: { queue: queueName, operation: 'closeInactive' } });
|
|
823
|
+
}
|
|
824
|
+
}),
|
|
825
|
+
);
|
|
826
|
+
|
|
827
|
+
updateBullBoard();
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function startCleanupInterval() {
|
|
831
|
+
if (cleanupIntervalId) clearInterval(cleanupIntervalId);
|
|
832
|
+
if (!isTestEnv) {
|
|
833
|
+
cleanupIntervalId = setInterval(closeInactiveQueues, inactivityCheckIntervalMs);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
async function startWorkersForExistingQueues() {
|
|
838
|
+
if (!connection) return;
|
|
839
|
+
|
|
840
|
+
const stream = connection.scanStream({ match: 'bull:*:meta', count: 1000 });
|
|
841
|
+
const potentialQueueNames = new Set();
|
|
842
|
+
|
|
843
|
+
stream.on('data', (keys) => {
|
|
844
|
+
(keys || []).forEach((key) => {
|
|
845
|
+
const match = key.match(/^bull:(.+):meta$/);
|
|
846
|
+
if (match && match[1]) potentialQueueNames.add(match[1]);
|
|
847
|
+
});
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
try {
|
|
851
|
+
await new Promise((resolve, reject) => { stream.on('end', resolve); stream.on('error', reject); });
|
|
852
|
+
} catch (error) {
|
|
853
|
+
Sentry.captureException(error, { tags: { operation: 'startWorkersForExistingQueues' } });
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (potentialQueueNames.size === 0) return;
|
|
858
|
+
|
|
859
|
+
const sortedConfigKeys = Array.from(jobConfigs.keys()).sort((a, b) => b.length - a.length);
|
|
860
|
+
|
|
861
|
+
await Promise.all(
|
|
862
|
+
Array.from(potentialQueueNames).map(async (fullQueueName) => {
|
|
863
|
+
let baseName = null;
|
|
864
|
+
let groupId = null;
|
|
865
|
+
let config = null;
|
|
866
|
+
|
|
867
|
+
sortedConfigKeys.some((potentialBase) => {
|
|
868
|
+
const baseConfig = jobConfigs.get(potentialBase);
|
|
869
|
+
if (fullQueueName === potentialBase && !baseConfig.group) {
|
|
870
|
+
baseName = potentialBase;
|
|
871
|
+
config = baseConfig;
|
|
872
|
+
return true;
|
|
873
|
+
}
|
|
874
|
+
if (baseConfig.group && fullQueueName.startsWith(\`\${potentialBase}-\`)) {
|
|
875
|
+
baseName = potentialBase;
|
|
876
|
+
groupId = fullQueueName.slice(potentialBase.length + 1);
|
|
877
|
+
config = baseConfig;
|
|
878
|
+
return true;
|
|
879
|
+
}
|
|
880
|
+
return false;
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
if (!config || activeQueues.has(fullQueueName)) return;
|
|
884
|
+
|
|
885
|
+
try {
|
|
886
|
+
const activeCount = await connection.llen(\`bull:\${fullQueueName}:active\`);
|
|
887
|
+
const waitCount = await connection.llen(\`bull:\${fullQueueName}:wait\`);
|
|
888
|
+
const delayedCount = await connection.zcard(\`bull:\${fullQueueName}:delayed\`);
|
|
889
|
+
|
|
890
|
+
if (waitCount > 0 || delayedCount > 0 || activeCount > 0) {
|
|
891
|
+
await getOrCreateQueueAndWorker(baseName, groupId, config);
|
|
892
|
+
}
|
|
893
|
+
} catch (error) {
|
|
894
|
+
Sentry.captureException(error, { tags: { queue: fullQueueName, operation: 'startupScanInit' } });
|
|
895
|
+
}
|
|
896
|
+
}),
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
async function initializeQueues(services, socketIo, bullBoardReplaceQueues) {
|
|
901
|
+
if (isTestEnv) { return { add: jest.fn() }; }
|
|
902
|
+
|
|
903
|
+
console.log('Queue system initializing...');
|
|
904
|
+
|
|
905
|
+
connection = createRedisConnection();
|
|
906
|
+
ioRef = socketIo;
|
|
907
|
+
bullBoardUpdateQueuesRef = bullBoardReplaceQueues;
|
|
908
|
+
|
|
909
|
+
const { loadJobs } = require('langaro-api');
|
|
910
|
+
const jobs = loadJobs(services);
|
|
911
|
+
Object.entries(jobs).forEach(([name, job]) => {
|
|
912
|
+
jobConfigs.set(name, {
|
|
913
|
+
handle: job.handle,
|
|
914
|
+
jobOptions: job.jobOptions || {},
|
|
915
|
+
workerOptions: job.workerOptions || {},
|
|
916
|
+
group: job.group,
|
|
917
|
+
});
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
await startWorkersForExistingQueues();
|
|
921
|
+
startCleanupInterval();
|
|
922
|
+
|
|
923
|
+
console.log('Queue system initialized.');
|
|
924
|
+
return { add };
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
async function shutdownQueues() {
|
|
928
|
+
if (isShuttingDown) return;
|
|
929
|
+
isShuttingDown = true;
|
|
930
|
+
|
|
931
|
+
console.log('Gracefully shutting down queues...');
|
|
932
|
+
|
|
933
|
+
if (cleanupIntervalId) {
|
|
934
|
+
clearInterval(cleanupIntervalId);
|
|
935
|
+
cleanupIntervalId = null;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const activeQueuesValues = Array.from(activeQueues.values());
|
|
939
|
+
|
|
940
|
+
await Promise.allSettled(activeQueuesValues.map(async (qd) => {
|
|
941
|
+
try { await qd.worker.pause(); } catch (e) { console.error(\`Error pausing \${qd.name}:\`, e); }
|
|
942
|
+
}));
|
|
943
|
+
|
|
944
|
+
let retries = 0;
|
|
945
|
+
const maxRetries = 400;
|
|
946
|
+
while (retries++ < maxRetries) {
|
|
947
|
+
const stillActive = await Promise.all(activeQueuesValues.map((qd) => qd.queue.getActiveCount()));
|
|
948
|
+
if (stillActive.reduce((a, b) => a + b, 0) === 0) break;
|
|
949
|
+
await sleep(3000);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
await Promise.allSettled(activeQueuesValues.map(async (qd) => {
|
|
953
|
+
try {
|
|
954
|
+
await qd.worker.close();
|
|
955
|
+
await qd.queue.close();
|
|
956
|
+
} catch (e) {
|
|
957
|
+
Sentry.captureException(e, { tags: { queue: qd.name, operation: 'shutdown' } });
|
|
958
|
+
}
|
|
959
|
+
}));
|
|
960
|
+
|
|
961
|
+
activeQueues.clear();
|
|
962
|
+
jobConfigs.clear();
|
|
963
|
+
|
|
964
|
+
if (connection) {
|
|
965
|
+
try { await connection.quit(); } catch (e) {
|
|
966
|
+
Sentry.captureException(e, { tags: { service: 'redis', operation: 'quit' } });
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
console.log('Queue shutdown complete.');
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
module.exports = { initializeQueues, shutdownQueues };
|
|
974
|
+
`;
|
|
975
|
+
|
|
651
976
|
const MODELS_INDEX = `const { loadModels } = require('langaro-api');
|
|
652
977
|
|
|
653
978
|
module.exports = (knexInstance) => loadModels(knexInstance, __dirname);
|
|
@@ -726,12 +1051,16 @@ async function run() {
|
|
|
726
1051
|
writeFile(root, 'jest.config.js', JEST_CONFIG);
|
|
727
1052
|
writeFile(root, '.gitignore', GITIGNORE);
|
|
728
1053
|
writeFile(root, '.env.example', envContent(port, dbName, redisPort));
|
|
729
|
-
writeFile(root, '.env', envContent(port, dbName, redisPort
|
|
1054
|
+
writeFile(root, '.env', envContent(port, dbName, redisPort, {
|
|
1055
|
+
jwtSecret: generateUUID(),
|
|
1056
|
+
masterPass: generateRandomString(24),
|
|
1057
|
+
}));
|
|
730
1058
|
|
|
731
1059
|
// src/config/
|
|
732
1060
|
writeFile(root, 'src/config/server.js', serverJs(port));
|
|
733
1061
|
writeFile(root, 'src/config/app.js', APP_JS);
|
|
734
1062
|
writeFile(root, 'src/config/instrument.js', INSTRUMENT_JS);
|
|
1063
|
+
writeFile(root, 'src/config/queues.js', QUEUES_JS);
|
|
735
1064
|
writeFile(root, 'src/config/tests/global-setup-tests.js', GLOBAL_SETUP_TESTS);
|
|
736
1065
|
writeFile(root, 'src/config/tests/setup-tests.js', SETUP_TESTS);
|
|
737
1066
|
|
|
@@ -753,6 +1082,7 @@ async function run() {
|
|
|
753
1082
|
|
|
754
1083
|
// src/utils
|
|
755
1084
|
writeFile(root, 'src/utils/index.js', UTILS_INDEX);
|
|
1085
|
+
writeFile(root, 'src/utils/sleep.js', UTILS_SLEEP);
|
|
756
1086
|
|
|
757
1087
|
// Empty directories
|
|
758
1088
|
['src/integrations', 'src/static', 'src/temp', 'src/database/tests'].forEach((dir) => {
|
|
@@ -762,7 +1092,7 @@ async function run() {
|
|
|
762
1092
|
console.log(`\n\x1b[32mProject scaffolded!\x1b[0m\n`);
|
|
763
1093
|
console.log('Next steps:');
|
|
764
1094
|
console.log(` 1. \x1b[36mnpm install\x1b[0m`);
|
|
765
|
-
console.log(` 2. Fill in your
|
|
1095
|
+
console.log(` 2. Fill in your \x1b[36m.env\x1b[0m (DB_PASSWORD, etc.)`);
|
|
766
1096
|
console.log(` 3. Create the \x1b[36m${dbName}\x1b[0m database in MySQL`);
|
|
767
1097
|
console.log(` 4. \x1b[36mnpm run dev\x1b[0m`);
|
|
768
1098
|
console.log(` 5. \x1b[36mnpx langaro-api new\x1b[0m to create your first resource\n`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "langaro-api",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
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,7 +21,7 @@
|
|
|
21
21
|
"boilerplate"
|
|
22
22
|
],
|
|
23
23
|
"peerDependencies": {
|
|
24
|
-
"knex-extended-crud": ">=2.0.
|
|
24
|
+
"knex-extended-crud": ">=2.0.31",
|
|
25
25
|
"cron": ">=3.0.0"
|
|
26
26
|
},
|
|
27
27
|
"peerDependenciesMeta": {
|