vg-coder-cli 1.0.0
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/README.md +179 -0
- package/bin/vg-coder.js +11 -0
- package/package.json +64 -0
- package/src/detectors/project-detector.js +333 -0
- package/src/exporter/html-exporter.js +1026 -0
- package/src/ignore/ignore-manager.js +298 -0
- package/src/index.js +282 -0
- package/src/scanner/file-scanner.js +592 -0
- package/src/tokenizer/token-manager.js +389 -0
- package/src/utils/helpers.js +128 -0
- package/test-project/package.json +21 -0
- package/test-project/src/controllers/userController.js +129 -0
- package/test-project/src/index.js +46 -0
- package/test-project/src/middleware/auth.js +142 -0
- package/test-project/styles/main.css +287 -0
- package/vg-coder-cli-1.0.0.tgz +0 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
const { encoding_for_model, get_encoding } = require('tiktoken');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Quản lý token counting và chunking
|
|
5
|
+
*/
|
|
6
|
+
class TokenManager {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.options = {
|
|
9
|
+
model: options.model || 'gpt-4',
|
|
10
|
+
maxTokens: options.maxTokens || 8000,
|
|
11
|
+
chunkOverlap: options.chunkOverlap || 200,
|
|
12
|
+
preserveStructure: options.preserveStructure !== false,
|
|
13
|
+
...options
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
this.encoding = null;
|
|
17
|
+
this.initializeEncoding();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Khởi tạo encoding
|
|
22
|
+
*/
|
|
23
|
+
initializeEncoding() {
|
|
24
|
+
try {
|
|
25
|
+
// Thử sử dụng encoding cho model cụ thể
|
|
26
|
+
this.encoding = encoding_for_model(this.options.model);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
try {
|
|
29
|
+
// Fallback to cl100k_base (GPT-4, GPT-3.5-turbo)
|
|
30
|
+
this.encoding = get_encoding('cl100k_base');
|
|
31
|
+
} catch (fallbackError) {
|
|
32
|
+
// Fallback to p50k_base (GPT-3)
|
|
33
|
+
this.encoding = get_encoding('p50k_base');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Đếm tokens trong text
|
|
40
|
+
*/
|
|
41
|
+
countTokens(text) {
|
|
42
|
+
if (!text || typeof text !== 'string') {
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const tokens = this.encoding.encode(text);
|
|
48
|
+
return tokens.length;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
// Fallback: estimate based on characters (rough approximation)
|
|
51
|
+
return Math.ceil(text.length / 4);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Phân tích token usage cho một file
|
|
57
|
+
*/
|
|
58
|
+
analyzeFile(file) {
|
|
59
|
+
const headerTokens = this.countTokens(this.generateFileHeader(file));
|
|
60
|
+
const contentTokens = this.countTokens(file.content);
|
|
61
|
+
const totalTokens = headerTokens + contentTokens;
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
file: file.relativePath,
|
|
65
|
+
headerTokens,
|
|
66
|
+
contentTokens,
|
|
67
|
+
totalTokens,
|
|
68
|
+
exceedsLimit: totalTokens > this.options.maxTokens,
|
|
69
|
+
chunksNeeded: Math.ceil(totalTokens / this.options.maxTokens)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Phân tích token usage cho tất cả files
|
|
75
|
+
*/
|
|
76
|
+
analyzeFiles(files) {
|
|
77
|
+
const analyses = files.map(file => this.analyzeFile(file));
|
|
78
|
+
|
|
79
|
+
const totalTokens = analyses.reduce((sum, analysis) => sum + analysis.totalTokens, 0);
|
|
80
|
+
const filesExceedingLimit = analyses.filter(analysis => analysis.exceedsLimit);
|
|
81
|
+
const totalChunks = analyses.reduce((sum, analysis) => sum + analysis.chunksNeeded, 0);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
files: analyses,
|
|
85
|
+
summary: {
|
|
86
|
+
totalFiles: files.length,
|
|
87
|
+
totalTokens,
|
|
88
|
+
averageTokensPerFile: Math.round(totalTokens / files.length),
|
|
89
|
+
filesExceedingLimit: filesExceedingLimit.length,
|
|
90
|
+
totalChunks,
|
|
91
|
+
estimatedChunks: Math.ceil(totalTokens / this.options.maxTokens)
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Chia nhỏ content thành chunks
|
|
98
|
+
*/
|
|
99
|
+
async chunkContent(content, metadata = {}) {
|
|
100
|
+
const totalTokens = this.countTokens(content);
|
|
101
|
+
|
|
102
|
+
if (totalTokens <= this.options.maxTokens) {
|
|
103
|
+
return [{
|
|
104
|
+
content,
|
|
105
|
+
tokens: totalTokens,
|
|
106
|
+
chunkIndex: 0,
|
|
107
|
+
totalChunks: 1,
|
|
108
|
+
metadata
|
|
109
|
+
}];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return this.options.preserveStructure
|
|
113
|
+
? await this.chunkByStructure(content, metadata)
|
|
114
|
+
: await this.chunkByTokens(content, metadata);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Chia nhỏ theo cấu trúc (ưu tiên giữ nguyên files)
|
|
119
|
+
*/
|
|
120
|
+
async chunkByStructure(content, metadata = {}) {
|
|
121
|
+
const chunks = [];
|
|
122
|
+
const lines = content.split('\n');
|
|
123
|
+
let currentChunk = '';
|
|
124
|
+
let currentTokens = 0;
|
|
125
|
+
let chunkIndex = 0;
|
|
126
|
+
|
|
127
|
+
// Tìm file boundaries
|
|
128
|
+
const fileBoundaries = this.findFileBoundaries(lines);
|
|
129
|
+
|
|
130
|
+
for (let i = 0; i < fileBoundaries.length; i++) {
|
|
131
|
+
const boundary = fileBoundaries[i];
|
|
132
|
+
const fileContent = lines.slice(boundary.start, boundary.end).join('\n');
|
|
133
|
+
const fileTokens = this.countTokens(fileContent);
|
|
134
|
+
|
|
135
|
+
// Nếu file này làm chunk vượt quá limit
|
|
136
|
+
if (currentTokens + fileTokens > this.options.maxTokens && currentChunk) {
|
|
137
|
+
// Lưu chunk hiện tại
|
|
138
|
+
chunks.push({
|
|
139
|
+
content: currentChunk.trim(),
|
|
140
|
+
tokens: currentTokens,
|
|
141
|
+
chunkIndex: chunkIndex++,
|
|
142
|
+
totalChunks: 0, // Sẽ update sau
|
|
143
|
+
metadata: { ...metadata, type: 'structure' }
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
currentChunk = '';
|
|
147
|
+
currentTokens = 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Nếu file quá lớn, chia nhỏ file này
|
|
151
|
+
if (fileTokens > this.options.maxTokens) {
|
|
152
|
+
const fileChunks = await this.chunkLargeFile(fileContent, boundary.filePath);
|
|
153
|
+
chunks.push(...fileChunks.map(chunk => ({
|
|
154
|
+
...chunk,
|
|
155
|
+
chunkIndex: chunkIndex++,
|
|
156
|
+
metadata: { ...metadata, ...chunk.metadata, type: 'large-file' }
|
|
157
|
+
})));
|
|
158
|
+
} else {
|
|
159
|
+
currentChunk += fileContent + '\n';
|
|
160
|
+
currentTokens += fileTokens;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Thêm chunk cuối cùng
|
|
165
|
+
if (currentChunk.trim()) {
|
|
166
|
+
chunks.push({
|
|
167
|
+
content: currentChunk.trim(),
|
|
168
|
+
tokens: currentTokens,
|
|
169
|
+
chunkIndex: chunkIndex++,
|
|
170
|
+
totalChunks: 0,
|
|
171
|
+
metadata: { ...metadata, type: 'structure' }
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Update totalChunks
|
|
176
|
+
chunks.forEach(chunk => {
|
|
177
|
+
chunk.totalChunks = chunks.length;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return chunks;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Chia nhỏ theo tokens (simple splitting)
|
|
185
|
+
*/
|
|
186
|
+
async chunkByTokens(content, metadata = {}) {
|
|
187
|
+
const chunks = [];
|
|
188
|
+
const lines = content.split('\n');
|
|
189
|
+
let currentChunk = '';
|
|
190
|
+
let currentTokens = 0;
|
|
191
|
+
let chunkIndex = 0;
|
|
192
|
+
|
|
193
|
+
for (const line of lines) {
|
|
194
|
+
const lineTokens = this.countTokens(line + '\n');
|
|
195
|
+
|
|
196
|
+
if (currentTokens + lineTokens > this.options.maxTokens && currentChunk) {
|
|
197
|
+
// Lưu chunk hiện tại
|
|
198
|
+
chunks.push({
|
|
199
|
+
content: currentChunk.trim(),
|
|
200
|
+
tokens: currentTokens,
|
|
201
|
+
chunkIndex: chunkIndex++,
|
|
202
|
+
totalChunks: 0,
|
|
203
|
+
metadata: { ...metadata, type: 'token-based' }
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Bắt đầu chunk mới với overlap
|
|
207
|
+
if (this.options.chunkOverlap > 0) {
|
|
208
|
+
const overlapLines = this.getOverlapLines(currentChunk, this.options.chunkOverlap);
|
|
209
|
+
currentChunk = overlapLines;
|
|
210
|
+
currentTokens = this.countTokens(overlapLines);
|
|
211
|
+
} else {
|
|
212
|
+
currentChunk = '';
|
|
213
|
+
currentTokens = 0;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
currentChunk += line + '\n';
|
|
218
|
+
currentTokens += lineTokens;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Thêm chunk cuối cùng
|
|
222
|
+
if (currentChunk.trim()) {
|
|
223
|
+
chunks.push({
|
|
224
|
+
content: currentChunk.trim(),
|
|
225
|
+
tokens: currentTokens,
|
|
226
|
+
chunkIndex: chunkIndex++,
|
|
227
|
+
totalChunks: 0,
|
|
228
|
+
metadata: { ...metadata, type: 'token-based' }
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Update totalChunks
|
|
233
|
+
chunks.forEach(chunk => {
|
|
234
|
+
chunk.totalChunks = chunks.length;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return chunks;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Chia nhỏ file lớn
|
|
242
|
+
*/
|
|
243
|
+
async chunkLargeFile(fileContent, filePath) {
|
|
244
|
+
const lines = fileContent.split('\n');
|
|
245
|
+
const chunks = [];
|
|
246
|
+
let currentChunk = '';
|
|
247
|
+
let currentTokens = 0;
|
|
248
|
+
let chunkIndex = 0;
|
|
249
|
+
|
|
250
|
+
// Thêm header cho file
|
|
251
|
+
const header = `\n=== Large File: ${filePath} (Part {part}) ===\n`;
|
|
252
|
+
|
|
253
|
+
for (const line of lines) {
|
|
254
|
+
const lineTokens = this.countTokens(line + '\n');
|
|
255
|
+
|
|
256
|
+
if (currentTokens + lineTokens > this.options.maxTokens - 100 && currentChunk) { // Reserve 100 tokens for header
|
|
257
|
+
const partHeader = header.replace('{part}', (chunkIndex + 1).toString());
|
|
258
|
+
chunks.push({
|
|
259
|
+
content: partHeader + currentChunk.trim(),
|
|
260
|
+
tokens: this.countTokens(partHeader + currentChunk.trim()),
|
|
261
|
+
chunkIndex: chunkIndex++,
|
|
262
|
+
totalChunks: 0,
|
|
263
|
+
metadata: {
|
|
264
|
+
filePath,
|
|
265
|
+
partNumber: chunkIndex + 1,
|
|
266
|
+
type: 'large-file-part'
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
currentChunk = '';
|
|
271
|
+
currentTokens = 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
currentChunk += line + '\n';
|
|
275
|
+
currentTokens += lineTokens;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Thêm chunk cuối cùng
|
|
279
|
+
if (currentChunk.trim()) {
|
|
280
|
+
const partHeader = header.replace('{part}', (chunkIndex + 1).toString());
|
|
281
|
+
chunks.push({
|
|
282
|
+
content: partHeader + currentChunk.trim(),
|
|
283
|
+
tokens: this.countTokens(partHeader + currentChunk.trim()),
|
|
284
|
+
chunkIndex: chunkIndex++,
|
|
285
|
+
totalChunks: 0,
|
|
286
|
+
metadata: {
|
|
287
|
+
filePath,
|
|
288
|
+
partNumber: chunkIndex + 1,
|
|
289
|
+
type: 'large-file-part'
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Update totalChunks
|
|
295
|
+
chunks.forEach(chunk => {
|
|
296
|
+
chunk.totalChunks = chunks.length;
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
return chunks;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Tìm file boundaries trong content
|
|
304
|
+
*/
|
|
305
|
+
findFileBoundaries(lines) {
|
|
306
|
+
const boundaries = [];
|
|
307
|
+
let currentStart = 0;
|
|
308
|
+
let currentFilePath = null;
|
|
309
|
+
|
|
310
|
+
for (let i = 0; i < lines.length; i++) {
|
|
311
|
+
const line = lines[i];
|
|
312
|
+
|
|
313
|
+
// Tìm file header pattern
|
|
314
|
+
if (line.includes('================================================================================')) {
|
|
315
|
+
if (i + 1 < lines.length && lines[i + 1].startsWith('File: ')) {
|
|
316
|
+
// Lưu boundary trước đó
|
|
317
|
+
if (currentFilePath) {
|
|
318
|
+
boundaries.push({
|
|
319
|
+
start: currentStart,
|
|
320
|
+
end: i,
|
|
321
|
+
filePath: currentFilePath
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Bắt đầu file mới
|
|
326
|
+
currentStart = i;
|
|
327
|
+
currentFilePath = lines[i + 1].replace('File: ', '').trim();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Thêm boundary cuối cùng
|
|
333
|
+
if (currentFilePath) {
|
|
334
|
+
boundaries.push({
|
|
335
|
+
start: currentStart,
|
|
336
|
+
end: lines.length,
|
|
337
|
+
filePath: currentFilePath
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return boundaries;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Lấy overlap lines
|
|
346
|
+
*/
|
|
347
|
+
getOverlapLines(content, maxTokens) {
|
|
348
|
+
const lines = content.split('\n');
|
|
349
|
+
let overlapContent = '';
|
|
350
|
+
let tokens = 0;
|
|
351
|
+
|
|
352
|
+
// Lấy từ cuối lên
|
|
353
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
354
|
+
const line = lines[i] + '\n';
|
|
355
|
+
const lineTokens = this.countTokens(line);
|
|
356
|
+
|
|
357
|
+
if (tokens + lineTokens > maxTokens) {
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
overlapContent = line + overlapContent;
|
|
362
|
+
tokens += lineTokens;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return overlapContent;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Tạo file header
|
|
370
|
+
*/
|
|
371
|
+
generateFileHeader(file) {
|
|
372
|
+
return `
|
|
373
|
+
================================================================================
|
|
374
|
+
File: ${file.relativePath}
|
|
375
|
+
Size: ${file.size} bytes | Lines: ${file.lines}
|
|
376
|
+
================================================================================`;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Cleanup encoding
|
|
381
|
+
*/
|
|
382
|
+
cleanup() {
|
|
383
|
+
if (this.encoding) {
|
|
384
|
+
this.encoding.free();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
module.exports = TokenManager;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility helpers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Format bytes to human readable string
|
|
7
|
+
*/
|
|
8
|
+
function formatBytes(bytes) {
|
|
9
|
+
if (bytes === 0) return '0 Bytes';
|
|
10
|
+
const k = 1024;
|
|
11
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
12
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
13
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Format number with commas
|
|
18
|
+
*/
|
|
19
|
+
function formatNumber(num) {
|
|
20
|
+
return num.toLocaleString();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get file extension from filename
|
|
25
|
+
*/
|
|
26
|
+
function getFileExtension(filename) {
|
|
27
|
+
return filename.slice((filename.lastIndexOf('.') - 1 >>> 0) + 2);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if path is hidden (starts with dot)
|
|
32
|
+
*/
|
|
33
|
+
function isHidden(filePath) {
|
|
34
|
+
return filePath.split('/').some(part => part.startsWith('.'));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Sanitize filename for safe usage
|
|
39
|
+
*/
|
|
40
|
+
function sanitizeFilename(filename) {
|
|
41
|
+
return filename.replace(/[^a-z0-9]/gi, '_').toLowerCase();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Deep merge objects
|
|
46
|
+
*/
|
|
47
|
+
function deepMerge(target, source) {
|
|
48
|
+
const output = Object.assign({}, target);
|
|
49
|
+
if (isObject(target) && isObject(source)) {
|
|
50
|
+
Object.keys(source).forEach(key => {
|
|
51
|
+
if (isObject(source[key])) {
|
|
52
|
+
if (!(key in target))
|
|
53
|
+
Object.assign(output, { [key]: source[key] });
|
|
54
|
+
else
|
|
55
|
+
output[key] = deepMerge(target[key], source[key]);
|
|
56
|
+
} else {
|
|
57
|
+
Object.assign(output, { [key]: source[key] });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return output;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if value is object
|
|
66
|
+
*/
|
|
67
|
+
function isObject(item) {
|
|
68
|
+
return item && typeof item === 'object' && !Array.isArray(item);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Debounce function
|
|
73
|
+
*/
|
|
74
|
+
function debounce(func, wait, immediate) {
|
|
75
|
+
let timeout;
|
|
76
|
+
return function executedFunction(...args) {
|
|
77
|
+
const later = () => {
|
|
78
|
+
timeout = null;
|
|
79
|
+
if (!immediate) func(...args);
|
|
80
|
+
};
|
|
81
|
+
const callNow = immediate && !timeout;
|
|
82
|
+
clearTimeout(timeout);
|
|
83
|
+
timeout = setTimeout(later, wait);
|
|
84
|
+
if (callNow) func(...args);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Throttle function
|
|
90
|
+
*/
|
|
91
|
+
function throttle(func, limit) {
|
|
92
|
+
let inThrottle;
|
|
93
|
+
return function(...args) {
|
|
94
|
+
if (!inThrottle) {
|
|
95
|
+
func.apply(this, args);
|
|
96
|
+
inThrottle = true;
|
|
97
|
+
setTimeout(() => inThrottle = false, limit);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Generate unique ID
|
|
104
|
+
*/
|
|
105
|
+
function generateId() {
|
|
106
|
+
return Math.random().toString(36).substr(2, 9);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Sleep for specified milliseconds
|
|
111
|
+
*/
|
|
112
|
+
function sleep(ms) {
|
|
113
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = {
|
|
117
|
+
formatBytes,
|
|
118
|
+
formatNumber,
|
|
119
|
+
getFileExtension,
|
|
120
|
+
isHidden,
|
|
121
|
+
sanitizeFilename,
|
|
122
|
+
deepMerge,
|
|
123
|
+
isObject,
|
|
124
|
+
debounce,
|
|
125
|
+
throttle,
|
|
126
|
+
generateId,
|
|
127
|
+
sleep
|
|
128
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "test-project",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A test project for VG Coder CLI",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node src/index.js",
|
|
8
|
+
"test": "jest"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"express": "^4.18.0",
|
|
12
|
+
"lodash": "^4.17.21"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"jest": "^29.0.0",
|
|
16
|
+
"nodemon": "^3.0.0"
|
|
17
|
+
},
|
|
18
|
+
"keywords": ["test", "demo", "vg-coder"],
|
|
19
|
+
"author": "VG Coder",
|
|
20
|
+
"license": "MIT"
|
|
21
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const _ = require('lodash');
|
|
3
|
+
const User = require('../models/User');
|
|
4
|
+
const { validateUser, validateEmail } = require('../utils/validation');
|
|
5
|
+
|
|
6
|
+
const router = express.Router();
|
|
7
|
+
|
|
8
|
+
// Get all users
|
|
9
|
+
router.get('/', async (req, res) => {
|
|
10
|
+
try {
|
|
11
|
+
const { page = 1, limit = 10, search } = req.query;
|
|
12
|
+
const offset = (page - 1) * limit;
|
|
13
|
+
|
|
14
|
+
let users = await User.findAll({
|
|
15
|
+
limit: parseInt(limit),
|
|
16
|
+
offset: parseInt(offset),
|
|
17
|
+
where: search ? {
|
|
18
|
+
name: { [Op.iLike]: `%${search}%` }
|
|
19
|
+
} : {}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
users = users.map(user => _.omit(user.toJSON(), ['password']));
|
|
23
|
+
|
|
24
|
+
res.json({
|
|
25
|
+
users,
|
|
26
|
+
pagination: {
|
|
27
|
+
page: parseInt(page),
|
|
28
|
+
limit: parseInt(limit),
|
|
29
|
+
total: await User.count()
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
} catch (error) {
|
|
33
|
+
res.status(500).json({ error: error.message });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Get user by ID
|
|
38
|
+
router.get('/:id', async (req, res) => {
|
|
39
|
+
try {
|
|
40
|
+
const { id } = req.params;
|
|
41
|
+
const user = await User.findByPk(id);
|
|
42
|
+
|
|
43
|
+
if (!user) {
|
|
44
|
+
return res.status(404).json({ error: 'User not found' });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
res.json(_.omit(user.toJSON(), ['password']));
|
|
48
|
+
} catch (error) {
|
|
49
|
+
res.status(500).json({ error: error.message });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Create new user
|
|
54
|
+
router.post('/', async (req, res) => {
|
|
55
|
+
try {
|
|
56
|
+
const userData = req.body;
|
|
57
|
+
|
|
58
|
+
// Validate input
|
|
59
|
+
const validation = validateUser(userData);
|
|
60
|
+
if (!validation.isValid) {
|
|
61
|
+
return res.status(400).json({
|
|
62
|
+
error: 'Validation failed',
|
|
63
|
+
details: validation.errors
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check if email already exists
|
|
68
|
+
const existingUser = await User.findOne({
|
|
69
|
+
where: { email: userData.email }
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (existingUser) {
|
|
73
|
+
return res.status(409).json({
|
|
74
|
+
error: 'Email already exists'
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const user = await User.create(userData);
|
|
79
|
+
res.status(201).json(_.omit(user.toJSON(), ['password']));
|
|
80
|
+
} catch (error) {
|
|
81
|
+
res.status(500).json({ error: error.message });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Update user
|
|
86
|
+
router.put('/:id', async (req, res) => {
|
|
87
|
+
try {
|
|
88
|
+
const { id } = req.params;
|
|
89
|
+
const updateData = req.body;
|
|
90
|
+
|
|
91
|
+
const user = await User.findByPk(id);
|
|
92
|
+
if (!user) {
|
|
93
|
+
return res.status(404).json({ error: 'User not found' });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Validate update data
|
|
97
|
+
const validation = validateUser(updateData, true);
|
|
98
|
+
if (!validation.isValid) {
|
|
99
|
+
return res.status(400).json({
|
|
100
|
+
error: 'Validation failed',
|
|
101
|
+
details: validation.errors
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await user.update(updateData);
|
|
106
|
+
res.json(_.omit(user.toJSON(), ['password']));
|
|
107
|
+
} catch (error) {
|
|
108
|
+
res.status(500).json({ error: error.message });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Delete user
|
|
113
|
+
router.delete('/:id', async (req, res) => {
|
|
114
|
+
try {
|
|
115
|
+
const { id } = req.params;
|
|
116
|
+
const user = await User.findByPk(id);
|
|
117
|
+
|
|
118
|
+
if (!user) {
|
|
119
|
+
return res.status(404).json({ error: 'User not found' });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await user.destroy();
|
|
123
|
+
res.status(204).send();
|
|
124
|
+
} catch (error) {
|
|
125
|
+
res.status(500).json({ error: error.message });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
module.exports = router;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const _ = require('lodash');
|
|
3
|
+
const userController = require('./controllers/userController');
|
|
4
|
+
const authMiddleware = require('./middleware/auth');
|
|
5
|
+
|
|
6
|
+
const app = express();
|
|
7
|
+
const PORT = process.env.PORT || 3000;
|
|
8
|
+
|
|
9
|
+
// Middleware
|
|
10
|
+
app.use(express.json());
|
|
11
|
+
app.use(express.urlencoded({ extended: true }));
|
|
12
|
+
|
|
13
|
+
// Routes
|
|
14
|
+
app.get('/', (req, res) => {
|
|
15
|
+
res.json({
|
|
16
|
+
message: 'Welcome to Test API',
|
|
17
|
+
version: '1.0.0',
|
|
18
|
+
timestamp: new Date().toISOString()
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
app.use('/api/users', authMiddleware, userController);
|
|
23
|
+
|
|
24
|
+
// Error handling middleware
|
|
25
|
+
app.use((err, req, res, next) => {
|
|
26
|
+
console.error(err.stack);
|
|
27
|
+
res.status(500).json({
|
|
28
|
+
error: 'Something went wrong!',
|
|
29
|
+
message: err.message
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// 404 handler
|
|
34
|
+
app.use('*', (req, res) => {
|
|
35
|
+
res.status(404).json({
|
|
36
|
+
error: 'Route not found',
|
|
37
|
+
path: req.originalUrl
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
app.listen(PORT, () => {
|
|
42
|
+
console.log(`Server is running on port ${PORT}`);
|
|
43
|
+
console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
module.exports = app;
|