langaro-api 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/bin/langaro-api.js +55 -0
- package/lib/generators/controllers.js +63 -0
- package/lib/generators/crud.js +157 -0
- package/lib/generators/index-dts.js +9 -0
- package/lib/generators/services.js +86 -0
- package/lib/index.js +82 -0
- package/lib/inject.js +123 -0
- package/lib/utils.js +69 -0
- package/package.json +27 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { generateTypes, getWatchDirs } = require('../lib/index');
|
|
6
|
+
|
|
7
|
+
// Load config from langaro-api.config.js at cwd, or use defaults
|
|
8
|
+
function loadConfig() {
|
|
9
|
+
const configPath = path.resolve(process.cwd(), 'langaro-api.config.js');
|
|
10
|
+
if (fs.existsSync(configPath)) {
|
|
11
|
+
return require(configPath);
|
|
12
|
+
}
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function run() {
|
|
17
|
+
const config = loadConfig();
|
|
18
|
+
generateTypes(config);
|
|
19
|
+
console.log('[langaro-api] Types + JSDoc annotations generated.');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Initial run
|
|
23
|
+
run();
|
|
24
|
+
|
|
25
|
+
// Watch mode
|
|
26
|
+
if (process.argv.includes('--watch')) {
|
|
27
|
+
const config = loadConfig();
|
|
28
|
+
const dirs = getWatchDirs(config);
|
|
29
|
+
|
|
30
|
+
let debounceTimer = null;
|
|
31
|
+
|
|
32
|
+
function onChange() {
|
|
33
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
34
|
+
debounceTimer = setTimeout(() => {
|
|
35
|
+
try {
|
|
36
|
+
run();
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error('[langaro-api] Error:', err.message);
|
|
39
|
+
}
|
|
40
|
+
}, 300);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
dirs.forEach((dir) => {
|
|
44
|
+
try {
|
|
45
|
+
fs.watch(dir, { recursive: true }, (eventType, filename) => {
|
|
46
|
+
if (filename && filename.endsWith('.js')) onChange();
|
|
47
|
+
});
|
|
48
|
+
} catch (err) {
|
|
49
|
+
// Directory might not support recursive watching on some platforms
|
|
50
|
+
console.warn(`[langaro-api] Could not watch ${dir}: ${err.message}`);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
console.log(`[langaro-api] Watching ${dirs.length} directories for changes...`);
|
|
55
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { pascalCase, extractMethods } = require('../utils');
|
|
4
|
+
|
|
5
|
+
function scanControllerEntries(controllersDir) {
|
|
6
|
+
const entries = [];
|
|
7
|
+
if (!fs.existsSync(controllersDir)) return entries;
|
|
8
|
+
|
|
9
|
+
fs.readdirSync(controllersDir).forEach((entry) => {
|
|
10
|
+
const fullPath = path.join(controllersDir, entry);
|
|
11
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
12
|
+
fs.readdirSync(fullPath)
|
|
13
|
+
.filter((f) => f.endsWith('.controller.js'))
|
|
14
|
+
.forEach((f) => entries.push({
|
|
15
|
+
tableName: f.replace('.controller.js', ''),
|
|
16
|
+
filePath: path.join(fullPath, f),
|
|
17
|
+
}));
|
|
18
|
+
} else if (entry.endsWith('.controller.js')) {
|
|
19
|
+
entries.push({ tableName: entry.replace('.controller.js', ''), filePath: fullPath });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
return entries.sort((a, b) => a.tableName.localeCompare(b.tableName));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function generateControllersDts(controllersDir) {
|
|
26
|
+
const lines = ['// Auto-generated by langaro-api — DO NOT EDIT', ''];
|
|
27
|
+
const controllerEntries = scanControllerEntries(controllersDir);
|
|
28
|
+
|
|
29
|
+
// Base classes for JSDoc @param on "ServicesClass"
|
|
30
|
+
controllerEntries.forEach(({ tableName }) => {
|
|
31
|
+
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
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Controller interfaces with their methods (for router autocomplete)
|
|
44
|
+
controllerEntries.forEach(({ tableName, filePath }) => {
|
|
45
|
+
const pc = pascalCase(tableName);
|
|
46
|
+
const methods = extractMethods(filePath);
|
|
47
|
+
lines.push(`declare interface I${pc}Controller extends ${pc}ControllerBase {`);
|
|
48
|
+
methods.forEach((m) => lines.push(` ${m}(...args: any[]): any;`));
|
|
49
|
+
lines.push('}', '');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// ControllersMap — used to type the "controllers" param in routers
|
|
53
|
+
lines.push('declare interface ControllersMap {');
|
|
54
|
+
controllerEntries.forEach(({ tableName }) => {
|
|
55
|
+
const pc = pascalCase(tableName);
|
|
56
|
+
lines.push(` ${pc}Controller: I${pc}Controller;`);
|
|
57
|
+
});
|
|
58
|
+
lines.push('}', '');
|
|
59
|
+
|
|
60
|
+
return lines.join('\n');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { scanControllerEntries, generateControllersDts };
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
module.exports = function generateCrudDts() {
|
|
2
|
+
return [
|
|
3
|
+
'// Auto-generated by langaro-api — DO NOT EDIT',
|
|
4
|
+
'',
|
|
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
|
+
'',
|
|
67
|
+
'declare interface CRUDGetOptions {',
|
|
68
|
+
' perPage?: number;',
|
|
69
|
+
' currentPage?: number;',
|
|
70
|
+
' append?: string[];',
|
|
71
|
+
' appendOptions?: Record<string, any>;',
|
|
72
|
+
' andWhere?: Array<[string, string, any]>;',
|
|
73
|
+
' search?: string;',
|
|
74
|
+
' searchExactMatch?: boolean;',
|
|
75
|
+
' searchFields?: string[];',
|
|
76
|
+
' firstOnly?: boolean;',
|
|
77
|
+
' show?: string[];',
|
|
78
|
+
' showOnly?: string[];',
|
|
79
|
+
' sortBy?: string;',
|
|
80
|
+
" sort?: 'asc' | 'desc';",
|
|
81
|
+
' where?: (query: any) => any;',
|
|
82
|
+
' [key: string]: any;',
|
|
83
|
+
'}',
|
|
84
|
+
'',
|
|
85
|
+
'declare interface CRUDUpdateOptions {',
|
|
86
|
+
' allowForbiddenUpdates?: boolean;',
|
|
87
|
+
' extraValidations?: Array<(data: any) => void>;',
|
|
88
|
+
' where?: (query: any) => any;',
|
|
89
|
+
' andWhere?: Array<[string, string, any]>;',
|
|
90
|
+
' whenSuccess?: (data: any) => void;',
|
|
91
|
+
' [key: string]: any;',
|
|
92
|
+
'}',
|
|
93
|
+
'',
|
|
94
|
+
'declare interface CRUDBatchUpdateOptions extends CRUDUpdateOptions {',
|
|
95
|
+
' getItemBy?: string;',
|
|
96
|
+
' socket?: any;',
|
|
97
|
+
' [key: string]: any;',
|
|
98
|
+
'}',
|
|
99
|
+
'',
|
|
100
|
+
'declare interface CRUDDeleteOptions {',
|
|
101
|
+
' where?: (query: any) => any;',
|
|
102
|
+
' andWhere?: Array<[string, string, any]>;',
|
|
103
|
+
' [key: string]: any;',
|
|
104
|
+
'}',
|
|
105
|
+
'',
|
|
106
|
+
'declare interface QueueMap {',
|
|
107
|
+
' add(name: string, data?: any, options?: any): Promise<any>;',
|
|
108
|
+
'}',
|
|
109
|
+
'',
|
|
110
|
+
'declare interface ModelFilterOption {',
|
|
111
|
+
' name: string;',
|
|
112
|
+
" type: 'select_multiple' | 'date' | 'is_not_null';",
|
|
113
|
+
' options?: any[];',
|
|
114
|
+
' column?: string;',
|
|
115
|
+
'}',
|
|
116
|
+
'',
|
|
117
|
+
'declare interface ModelConfig {',
|
|
118
|
+
' /** Fields hidden from API responses */',
|
|
119
|
+
' hide?: string[];',
|
|
120
|
+
' /** Relation mappings for appendItems */',
|
|
121
|
+
" append?: Record<string, 'one' | 'many' | string>;",
|
|
122
|
+
' /** Yup validation schema */',
|
|
123
|
+
' schema?: any;',
|
|
124
|
+
' /** Default sort column */',
|
|
125
|
+
' sortBy?: string;',
|
|
126
|
+
' /** Default sort direction */',
|
|
127
|
+
" sort?: 'asc' | 'desc';",
|
|
128
|
+
' /** Fields that are not searchable */',
|
|
129
|
+
' notSearchableFields?: string[];',
|
|
130
|
+
' /** Enable drag-and-drop sorting */',
|
|
131
|
+
' sortable?: boolean;',
|
|
132
|
+
' /** Force integer conversion for these fields */',
|
|
133
|
+
' forceInteger?: string[];',
|
|
134
|
+
' /** Override singular table name */',
|
|
135
|
+
' singularTableName?: string;',
|
|
136
|
+
' /** Override plural table name */',
|
|
137
|
+
' pluralTableName?: string;',
|
|
138
|
+
' /** Field specifications */',
|
|
139
|
+
' fieldsSpec?: any[];',
|
|
140
|
+
' /** Cache timeout in ms (default: 300000) */',
|
|
141
|
+
' cacheTimeout?: number;',
|
|
142
|
+
' /** Enable/disable schema cache */',
|
|
143
|
+
' cacheEnabled?: boolean;',
|
|
144
|
+
' fields?: {',
|
|
145
|
+
' /** Fields not required when creating a record */',
|
|
146
|
+
' notRequiredOnCreate?: string[];',
|
|
147
|
+
' /** Fields that cannot be updated */',
|
|
148
|
+
' notUpdatable?: string[];',
|
|
149
|
+
' /** Searchable fields for text search */',
|
|
150
|
+
' searchableFields?: string[];',
|
|
151
|
+
' /** Filter options for list queries */',
|
|
152
|
+
' filterOptions?: ModelFilterOption[];',
|
|
153
|
+
' };',
|
|
154
|
+
'}',
|
|
155
|
+
'',
|
|
156
|
+
].join('\n');
|
|
157
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module.exports = function generateIndexDts() {
|
|
2
|
+
return [
|
|
3
|
+
'// Auto-generated by langaro-api — DO NOT EDIT',
|
|
4
|
+
'/// <reference path="./crud.d.ts" />',
|
|
5
|
+
'/// <reference path="./services.d.ts" />',
|
|
6
|
+
'/// <reference path="./controllers.d.ts" />',
|
|
7
|
+
'',
|
|
8
|
+
].join('\n');
|
|
9
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { pascalCase, extractMethods, isCustomService } = require('../utils');
|
|
4
|
+
|
|
5
|
+
module.exports = function generateServicesDts(servicesDir, modelsDir) {
|
|
6
|
+
const lines = ['// Auto-generated by langaro-api — DO NOT EDIT', ''];
|
|
7
|
+
const serviceEntries = [];
|
|
8
|
+
|
|
9
|
+
// Services with custom .services.js files
|
|
10
|
+
if (fs.existsSync(servicesDir)) {
|
|
11
|
+
fs.readdirSync(servicesDir)
|
|
12
|
+
.filter((f) => f.endsWith('.services.js'))
|
|
13
|
+
.sort()
|
|
14
|
+
.forEach((file) => {
|
|
15
|
+
const tableName = file.replace('.services.js', '');
|
|
16
|
+
const serviceName = `${pascalCase(tableName)}Services`;
|
|
17
|
+
const interfaceName = `I${serviceName}`;
|
|
18
|
+
const filePath = path.join(servicesDir, file);
|
|
19
|
+
const custom = isCustomService(filePath);
|
|
20
|
+
const methods = extractMethods(filePath);
|
|
21
|
+
|
|
22
|
+
if (custom) {
|
|
23
|
+
lines.push(`declare interface ${interfaceName} {`);
|
|
24
|
+
lines.push(' table: string;');
|
|
25
|
+
} else {
|
|
26
|
+
lines.push(`declare interface ${interfaceName} extends CRUD {`);
|
|
27
|
+
}
|
|
28
|
+
methods.forEach((m) => lines.push(` ${m}(...args: any[]): any;`));
|
|
29
|
+
lines.push('}', '');
|
|
30
|
+
|
|
31
|
+
serviceEntries.push({
|
|
32
|
+
interfaceName,
|
|
33
|
+
serviceName,
|
|
34
|
+
tableName,
|
|
35
|
+
custom,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// DB tables with .model.js but no .services.js — pure CRUD
|
|
41
|
+
if (fs.existsSync(modelsDir)) {
|
|
42
|
+
fs.readdirSync(modelsDir)
|
|
43
|
+
.filter((f) => f.endsWith('.model.js'))
|
|
44
|
+
.forEach((file) => {
|
|
45
|
+
const tableName = file.replace('.model.js', '');
|
|
46
|
+
const serviceName = `${pascalCase(tableName)}Services`;
|
|
47
|
+
if (serviceEntries.find((e) => e.serviceName === serviceName)) return;
|
|
48
|
+
const interfaceName = `I${serviceName}`;
|
|
49
|
+
lines.push(`declare interface ${interfaceName} extends CRUD {}`, '');
|
|
50
|
+
serviceEntries.push({
|
|
51
|
+
interfaceName,
|
|
52
|
+
serviceName,
|
|
53
|
+
tableName,
|
|
54
|
+
custom: false,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ServicesMap
|
|
60
|
+
lines.push('declare interface ServicesMap {');
|
|
61
|
+
serviceEntries
|
|
62
|
+
.sort((a, b) => a.serviceName.localeCompare(b.serviceName))
|
|
63
|
+
.forEach(({ interfaceName, serviceName }) => {
|
|
64
|
+
lines.push(` ${serviceName}: ${interfaceName};`);
|
|
65
|
+
});
|
|
66
|
+
lines.push('}', '');
|
|
67
|
+
|
|
68
|
+
// ModelsConstructorMap — typed table names for "new models.xxx()"
|
|
69
|
+
lines.push('declare interface ModelsConstructorMap {');
|
|
70
|
+
serviceEntries
|
|
71
|
+
.filter(({ custom }) => !custom)
|
|
72
|
+
.sort((a, b) => a.tableName.localeCompare(b.tableName))
|
|
73
|
+
.forEach(({ tableName }) => {
|
|
74
|
+
lines.push(` ${tableName}: new () => CRUD;`);
|
|
75
|
+
});
|
|
76
|
+
lines.push(' [key: string]: new () => CRUD;');
|
|
77
|
+
lines.push('}', '');
|
|
78
|
+
|
|
79
|
+
// 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 {}`);
|
|
82
|
+
});
|
|
83
|
+
lines.push('');
|
|
84
|
+
|
|
85
|
+
return lines.join('\n');
|
|
86
|
+
};
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const generateCrudDts = require('./generators/crud');
|
|
5
|
+
const generateServicesDts = require('./generators/services');
|
|
6
|
+
const { generateControllersDts } = require('./generators/controllers');
|
|
7
|
+
const generateIndexDts = require('./generators/index-dts');
|
|
8
|
+
const {
|
|
9
|
+
injectControllerAnnotations,
|
|
10
|
+
injectServiceAnnotations,
|
|
11
|
+
injectRouterAnnotations,
|
|
12
|
+
injectModelAnnotations,
|
|
13
|
+
injectJobAnnotations,
|
|
14
|
+
injectTaskAnnotations,
|
|
15
|
+
injectMiddlewareAnnotations,
|
|
16
|
+
} = require('./inject');
|
|
17
|
+
|
|
18
|
+
const DEFAULTS = {
|
|
19
|
+
root: process.cwd(),
|
|
20
|
+
output: '@types/generated',
|
|
21
|
+
services: 'src/database/services',
|
|
22
|
+
models: 'src/database/models',
|
|
23
|
+
controllers: 'src/controllers',
|
|
24
|
+
routes: 'src/routes',
|
|
25
|
+
jobs: 'src/jobs',
|
|
26
|
+
tasks: 'src/tasks',
|
|
27
|
+
middlewares: 'src/middlewares',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function generateTypes(userConfig = {}) {
|
|
31
|
+
const config = { ...DEFAULTS, ...userConfig };
|
|
32
|
+
const root = config.root;
|
|
33
|
+
|
|
34
|
+
const resolve = (dir) => path.resolve(root, dir);
|
|
35
|
+
|
|
36
|
+
const outputDir = resolve(config.output);
|
|
37
|
+
const servicesDir = resolve(config.services);
|
|
38
|
+
const modelsDir = resolve(config.models);
|
|
39
|
+
const controllersDir = resolve(config.controllers);
|
|
40
|
+
const routesDir = resolve(config.routes);
|
|
41
|
+
const jobsDir = resolve(config.jobs);
|
|
42
|
+
const tasksDir = resolve(config.tasks);
|
|
43
|
+
const middlewaresDir = resolve(config.middlewares);
|
|
44
|
+
|
|
45
|
+
// Generate .d.ts files
|
|
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));
|
|
50
|
+
fs.writeFileSync(path.join(outputDir, 'index.d.ts'), generateIndexDts());
|
|
51
|
+
|
|
52
|
+
// Inject JSDoc annotations into source files
|
|
53
|
+
injectControllerAnnotations(controllersDir);
|
|
54
|
+
injectServiceAnnotations(servicesDir);
|
|
55
|
+
injectRouterAnnotations(routesDir);
|
|
56
|
+
injectModelAnnotations(modelsDir);
|
|
57
|
+
injectJobAnnotations(jobsDir);
|
|
58
|
+
injectTaskAnnotations(tasksDir);
|
|
59
|
+
injectMiddlewareAnnotations(middlewaresDir);
|
|
60
|
+
|
|
61
|
+
return { outputDir, config };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getWatchDirs(userConfig = {}) {
|
|
65
|
+
const config = { ...DEFAULTS, ...userConfig };
|
|
66
|
+
const root = config.root;
|
|
67
|
+
const resolve = (dir) => path.resolve(root, dir);
|
|
68
|
+
|
|
69
|
+
return [
|
|
70
|
+
config.services,
|
|
71
|
+
config.models,
|
|
72
|
+
config.controllers,
|
|
73
|
+
config.routes,
|
|
74
|
+
config.jobs,
|
|
75
|
+
config.tasks,
|
|
76
|
+
config.middlewares,
|
|
77
|
+
]
|
|
78
|
+
.map(resolve)
|
|
79
|
+
.filter((dir) => fs.existsSync(dir));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { generateTypes, getWatchDirs, DEFAULTS };
|
package/lib/inject.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { JSDOC_MARKER, pascalCase, isCustomService, scanJsFilesRecursively } = require('./utils');
|
|
4
|
+
const { scanControllerEntries } = require('./generators/controllers');
|
|
5
|
+
|
|
6
|
+
function injectJSDoc(filePath, jsdocComment, targetLineRegex) {
|
|
7
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
8
|
+
const lines = content.split('\n');
|
|
9
|
+
|
|
10
|
+
const targetIdx = lines.findIndex((line) => targetLineRegex.test(line));
|
|
11
|
+
if (targetIdx === -1) return false;
|
|
12
|
+
|
|
13
|
+
// Search up to 3 lines above target for existing marker (handles eslint-disable comments between)
|
|
14
|
+
let existingIdx = -1;
|
|
15
|
+
const searchRange = Math.min(3, targetIdx);
|
|
16
|
+
for (let i = 1; i <= searchRange; i += 1) {
|
|
17
|
+
if (lines[targetIdx - i].includes(JSDOC_MARKER)) {
|
|
18
|
+
existingIdx = targetIdx - i;
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (existingIdx !== -1) {
|
|
24
|
+
if (lines[existingIdx] === jsdocComment) return true; // already up-to-date
|
|
25
|
+
lines[existingIdx] = jsdocComment;
|
|
26
|
+
} else {
|
|
27
|
+
lines.splice(targetIdx, 0, jsdocComment);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fs.writeFileSync(filePath, lines.join('\n'));
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function injectControllerAnnotations(controllersDir) {
|
|
35
|
+
scanControllerEntries(controllersDir).forEach(({ filePath, tableName }) => {
|
|
36
|
+
const baseName = `${pascalCase(tableName)}ControllerBase`;
|
|
37
|
+
const jsdoc = `/** @param {typeof ${baseName}} ServicesClass ${JSDOC_MARKER} */`;
|
|
38
|
+
injectJSDoc(filePath, jsdoc, /module\.exports\s*=\s*\(\s*ServicesClass\s*\)/);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function injectRouterAnnotations(routesDir) {
|
|
43
|
+
if (!fs.existsSync(routesDir)) return;
|
|
44
|
+
fs.readdirSync(routesDir)
|
|
45
|
+
.filter((f) => f.endsWith('.router.js'))
|
|
46
|
+
.forEach((file) => {
|
|
47
|
+
const filePath = path.join(routesDir, file);
|
|
48
|
+
const jsdocBoth = `/** @param {ControllersMap} controllers @param {ServicesMap} services ${JSDOC_MARKER} */`;
|
|
49
|
+
if (injectJSDoc(filePath, jsdocBoth, /module\.exports\s*=\s*\(\s*controllers\s*,\s*services\s*\)/)) return;
|
|
50
|
+
const jsdocOnly = `/** @param {ControllersMap} controllers ${JSDOC_MARKER} */`;
|
|
51
|
+
injectJSDoc(filePath, jsdocOnly, /module\.exports\s*=\s*\(\s*controllers\s*\)/);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function injectMiddlewareAnnotations(middlewaresDir) {
|
|
56
|
+
if (!fs.existsSync(middlewaresDir)) return;
|
|
57
|
+
fs.readdirSync(middlewaresDir)
|
|
58
|
+
.filter((f) => f.endsWith('.js'))
|
|
59
|
+
.forEach((file) => {
|
|
60
|
+
const filePath = path.join(middlewaresDir, file);
|
|
61
|
+
const jsdoc = `/** @param {ServicesMap} services ${JSDOC_MARKER} */`;
|
|
62
|
+
injectJSDoc(filePath, jsdoc, /module\.exports\s*=\s*(function\s*)?\(\s*services\s*\)/);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function injectModelAnnotations(modelsDir) {
|
|
67
|
+
if (!fs.existsSync(modelsDir)) return;
|
|
68
|
+
fs.readdirSync(modelsDir)
|
|
69
|
+
.filter((f) => f.endsWith('.model.js'))
|
|
70
|
+
.forEach((file) => {
|
|
71
|
+
const filePath = path.join(modelsDir, file);
|
|
72
|
+
const jsdoc = `/** @type {ModelConfig} ${JSDOC_MARKER} */`;
|
|
73
|
+
injectJSDoc(filePath, jsdoc, /module\.exports\s*=\s*\{/);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function injectJobAnnotations(jobsDir) {
|
|
78
|
+
if (!fs.existsSync(jobsDir)) return;
|
|
79
|
+
scanJsFilesRecursively(jobsDir).forEach((filePath) => {
|
|
80
|
+
const jsdoc = `/** @param {ServicesMap} services ${JSDOC_MARKER} */`;
|
|
81
|
+
injectJSDoc(filePath, jsdoc, /module\.exports\s*=\s*\(\s*services\s*\)\s*=>\s*\(\{/);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function injectTaskAnnotations(tasksDir) {
|
|
86
|
+
if (!fs.existsSync(tasksDir)) return;
|
|
87
|
+
scanJsFilesRecursively(tasksDir).forEach((filePath) => {
|
|
88
|
+
const jsdocWithQueue = `/** @param {ServicesMap} services @param {QueueMap} queue ${JSDOC_MARKER} */`;
|
|
89
|
+
if (injectJSDoc(filePath, jsdocWithQueue, /module\.exports\s*=\s*\(\s*services\s*,\s*[qQ]ueue\s*\)\s*=>\s*\{/)) return;
|
|
90
|
+
const jsdocNoQueue = `/** @param {ServicesMap} services ${JSDOC_MARKER} */`;
|
|
91
|
+
injectJSDoc(filePath, jsdocNoQueue, /module\.exports\s*=\s*\(\s*services\s*\)\s*=>\s*\{/);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function injectServiceAnnotations(servicesDir) {
|
|
96
|
+
if (!fs.existsSync(servicesDir)) return;
|
|
97
|
+
fs.readdirSync(servicesDir)
|
|
98
|
+
.filter((f) => f.endsWith('.services.js'))
|
|
99
|
+
.forEach((file) => {
|
|
100
|
+
const filePath = path.join(servicesDir, file);
|
|
101
|
+
const tableName = file.replace('.services.js', '');
|
|
102
|
+
const custom = isCustomService(filePath);
|
|
103
|
+
|
|
104
|
+
if (custom) {
|
|
105
|
+
const jsdoc = `/** @param {ModelsConstructorMap} models ${JSDOC_MARKER} */`;
|
|
106
|
+
injectJSDoc(filePath, jsdoc, /module\.exports\s*=\s*\(\s*models\s*(?:,\s*io)?\s*\)\s*=>\s*class/);
|
|
107
|
+
} else {
|
|
108
|
+
const baseName = `${pascalCase(tableName)}ServicesBase`;
|
|
109
|
+
const jsdoc = `/** @param {typeof ${baseName}} model @param {ModelsConstructorMap} models ${JSDOC_MARKER} */`;
|
|
110
|
+
injectJSDoc(filePath, jsdoc, /module\.exports\s*=\s*\(\s*model/);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = {
|
|
116
|
+
injectControllerAnnotations,
|
|
117
|
+
injectServiceAnnotations,
|
|
118
|
+
injectRouterAnnotations,
|
|
119
|
+
injectModelAnnotations,
|
|
120
|
+
injectJobAnnotations,
|
|
121
|
+
injectTaskAnnotations,
|
|
122
|
+
injectMiddlewareAnnotations,
|
|
123
|
+
};
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
const JSDOC_MARKER = '@generated-types';
|
|
4
|
+
|
|
5
|
+
const JS_RESERVED = new Set([
|
|
6
|
+
'break', 'case', 'catch', 'continue', 'debugger', 'default', 'delete',
|
|
7
|
+
'do', 'else', 'finally', 'for', 'function', 'if', 'in', 'instanceof',
|
|
8
|
+
'new', 'return', 'switch', 'this', 'throw', 'try', 'typeof', 'var',
|
|
9
|
+
'void', 'while', 'with', 'class', 'const', 'enum', 'export', 'extends',
|
|
10
|
+
'import', 'super', 'implements', 'interface', 'let', 'package', 'private',
|
|
11
|
+
'protected', 'public', 'static', 'yield', 'await', 'async',
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
function pascalCase(str) {
|
|
15
|
+
return str
|
|
16
|
+
.split(/[_\-\s]+/)
|
|
17
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
18
|
+
.join('');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function matchAll(regex, text) {
|
|
22
|
+
const results = [];
|
|
23
|
+
let match;
|
|
24
|
+
while ((match = regex.exec(text)) !== null) results.push(match);
|
|
25
|
+
return results;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function extractMethods(filePath) {
|
|
29
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
30
|
+
|
|
31
|
+
const patterns = [
|
|
32
|
+
/^\s+(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*\{/gm,
|
|
33
|
+
/this\.([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*[a-zA-Z_$][a-zA-Z0-9_$]*\.bind\(this\)/g,
|
|
34
|
+
/^async\s+function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm,
|
|
35
|
+
];
|
|
36
|
+
|
|
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
|
+
}
|
|
42
|
+
|
|
43
|
+
function isCustomService(filePath) {
|
|
44
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
45
|
+
return /module\.exports\s*=\s*\(\s*models\s*(?:,\s*io)?\s*\)\s*=>\s*class\s*\{/.test(content);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function scanJsFilesRecursively(dir) {
|
|
49
|
+
const results = [];
|
|
50
|
+
fs.readdirSync(dir).forEach((entry) => {
|
|
51
|
+
const fullPath = require('path').join(dir, entry);
|
|
52
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
53
|
+
results.push(...scanJsFilesRecursively(fullPath));
|
|
54
|
+
} else if (entry.endsWith('.js') && !entry.endsWith('.spec.js') && entry !== 'index.js') {
|
|
55
|
+
results.push(fullPath);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return results;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
JSDOC_MARKER,
|
|
63
|
+
JS_RESERVED,
|
|
64
|
+
pascalCase,
|
|
65
|
+
matchAll,
|
|
66
|
+
extractMethods,
|
|
67
|
+
isCustomService,
|
|
68
|
+
scanJsFilesRecursively,
|
|
69
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "langaro-api",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Auto-generate TypeScript types and JSDoc annotations for knex-extended-crud projects",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"langaro-api": "bin/langaro-api.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"lib/",
|
|
11
|
+
"bin/"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"knex",
|
|
15
|
+
"crud",
|
|
16
|
+
"types",
|
|
17
|
+
"jsdoc",
|
|
18
|
+
"intellisense",
|
|
19
|
+
"knex-extended-crud"
|
|
20
|
+
],
|
|
21
|
+
"author": "Fernando Langaro",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/fernandolangaro/langaro-api.git"
|
|
26
|
+
}
|
|
27
|
+
}
|