@azure-net/cli 2.3.62 → 4.0.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.
Files changed (91) hide show
  1. package/.prettierrc +7 -0
  2. package/bin/azure-net.js +247 -157
  3. package/bin/azure-net.ts +253 -0
  4. package/eslint.config.js +19 -0
  5. package/package.json +12 -5
  6. package/src/commands/generate/generateCrudBase.js +95 -114
  7. package/src/commands/generate/generateCrudPresenter.js +135 -142
  8. package/src/commands/generate/generateModule.js +21 -21
  9. package/src/commands/generate/generateModuleBase.js +92 -114
  10. package/src/commands/generate/generateModulePresenter.js +73 -80
  11. package/src/commands/generate/generatePresenter.js +38 -44
  12. package/src/commands/generate/generateRepository.js +41 -47
  13. package/src/commands/generate/generateSchema.js +74 -70
  14. package/src/commands/generate/generateService.js +32 -37
  15. package/src/commands/generate/index.js +0 -5
  16. package/src/generate/layers/generateRepository.js +124 -0
  17. package/src/generate/layers/generateRepositoryMethod.js +102 -0
  18. package/src/generate/layers/generateUseCases.js +50 -0
  19. package/src/generate/layers/repositoryShared.js +569 -0
  20. package/src/generate/module/generateDomain.js +146 -0
  21. package/src/generate/module/generateDomain.ts +147 -0
  22. package/src/generate/module/generateModule.js +84 -0
  23. package/src/generate/module/generateModule.ts +84 -0
  24. package/src/generate/module/generatePresenter.js +419 -0
  25. package/src/generate/module/generatePresenter.ts +442 -0
  26. package/src/generate/module/generateRepository.js +144 -0
  27. package/src/generate/module/generateRepository.ts +158 -0
  28. package/src/generate/module/generateSchemaFromType.js +320 -0
  29. package/src/generate/module/generateSchemaFromType.ts +325 -0
  30. package/src/generate/module/generateTypesJson.js +252 -0
  31. package/src/generate/module/generateTypesJson.ts +259 -0
  32. package/src/generate/module/generateUseCases.js +67 -0
  33. package/src/generate/module/generateUseCases.ts +67 -0
  34. package/src/generate/module/repositoryModuleShared.js +598 -0
  35. package/src/generate/ui/generateComponent.js +98 -0
  36. package/src/generate/ui/generateComponent.ts +100 -0
  37. package/src/generate/ui/generateDesignComponent.js +71 -0
  38. package/src/generate/ui/generateDesignComponent.ts +71 -0
  39. package/src/generate/ui/generateWidget.js +71 -0
  40. package/src/generate/ui/generateWidget.ts +71 -0
  41. package/src/init/initAliases.js +159 -0
  42. package/src/init/initAliases.ts +167 -0
  43. package/src/init/initEdges.js +74 -0
  44. package/src/init/initEdges.ts +74 -0
  45. package/src/init/initStructure.js +122 -0
  46. package/src/init/initStructure.ts +124 -0
  47. package/src/install/installFresh.js +101 -0
  48. package/src/install/installFresh.ts +101 -0
  49. package/src/plugins/addLint.js +257 -0
  50. package/src/plugins/addLint.ts +274 -0
  51. package/src/plugins/index.js +1 -0
  52. package/src/plugins/index.ts +1 -0
  53. package/src/scaffolds/createCoreMiddleware.js +27 -0
  54. package/src/scaffolds/createCoreMiddleware.ts +27 -0
  55. package/src/scaffolds/createCoreSchema.js +141 -0
  56. package/src/scaffolds/createCoreSchema.ts +142 -0
  57. package/src/scaffolds/createDatasource.js +173 -0
  58. package/src/scaffolds/createDatasource.ts +175 -0
  59. package/src/scaffolds/createDatasourceProvider.js +164 -0
  60. package/src/scaffolds/createDatasourceProvider.ts +175 -0
  61. package/src/scaffolds/createMiddleware.js +105 -0
  62. package/src/scaffolds/createMiddleware.ts +105 -0
  63. package/src/scaffolds/createPresenterFactory.js +112 -0
  64. package/src/scaffolds/createPresenterFactory.ts +114 -0
  65. package/src/scaffolds/createResponse.js +64 -0
  66. package/src/scaffolds/createResponse.ts +64 -0
  67. package/src/scaffolds/createSchemaRule.js +40 -0
  68. package/src/scaffolds/createSchemaRule.ts +42 -0
  69. package/src/scaffolds/createTranslationManager.js +78 -0
  70. package/src/scaffolds/createTranslationManager.ts +78 -0
  71. package/src/scaffolds/index.js +9 -0
  72. package/src/scaffolds/index.ts +9 -0
  73. package/src/utils/contextUtils.js +56 -49
  74. package/src/utils/fileUtils.js +72 -84
  75. package/src/utils/loadConfig.js +57 -13
  76. package/tsconfig.json +19 -0
  77. package/src/commands/generate/generateComponent.js +0 -53
  78. package/src/commands/generate/generateDatasource.js +0 -84
  79. package/src/commands/generate/generateDesignComponent.js +0 -53
  80. package/src/commands/generate/generateResponse.js +0 -41
  81. package/src/commands/generate/generateWidget.js +0 -53
  82. package/src/commands/init/index.js +0 -9
  83. package/src/commands/init/initAliases.js +0 -52
  84. package/src/commands/init/initEdges.js +0 -42
  85. package/src/commands/init/initLint.js +0 -138
  86. package/src/commands/init/initMiddleware.js +0 -21
  87. package/src/commands/init/initPresenter.js +0 -25
  88. package/src/commands/init/initSchema.js +0 -22
  89. package/src/commands/init/initStructure.js +0 -140
  90. package/src/commands/init/initTranslations.js +0 -59
  91. package/src/commands/init/migrateStructure.js +0 -144
@@ -0,0 +1,141 @@
1
+ import fs from 'node:fs/promises';
2
+ import fsSync from 'node:fs';
3
+ import path from 'node:path';
4
+ import prompts from 'prompts';
5
+ import { writeIfNotExists, updateIndexTs } from '../utils/fileUtils.js';
6
+ import { toPascalCase } from '../utils/contextUtils.js';
7
+
8
+ const resolvePresetImportName = (preset) => {
9
+ switch (preset) {
10
+ case 'ru':
11
+ return 'validationMessagesRu';
12
+ case 'en':
13
+ return 'validationMessagesEn';
14
+ case 'i18n':
15
+ return 'validationMessagesI18n';
16
+ default:
17
+ return 'validationMessagesI18n';
18
+ }
19
+ };
20
+
21
+ const createSchemaFactoryTemplate = (
22
+ factoryName,
23
+ messagesImportName
24
+ ) => `import { createSchemaFactory, createRules, ${messagesImportName} } from '@azure-net/kit/delivery';
25
+
26
+ export const ${factoryName} = createSchemaFactory(createRules(${messagesImportName}));
27
+ `;
28
+
29
+ const resolveConfigFile = (cwd) => {
30
+ const tsPath = path.join(cwd, 'azure-net.config.ts');
31
+ const jsPath = path.join(cwd, 'azure-net.config.js');
32
+
33
+ if (fsSync.existsSync(tsPath)) {
34
+ return { filepath: tsPath, type: 'ts', exists: true, content: fsSync.readFileSync(tsPath, 'utf-8') };
35
+ }
36
+ if (fsSync.existsSync(jsPath)) {
37
+ return { filepath: jsPath, type: 'js', exists: true, content: fsSync.readFileSync(jsPath, 'utf-8') };
38
+ }
39
+
40
+ return { filepath: tsPath, type: 'ts', exists: false, content: '' };
41
+ };
42
+
43
+ const buildDefaultSchemaFactoryConfigEntry = (factoryName) => {
44
+ return `defaultSchemaFactory: '${factoryName}'`;
45
+ };
46
+
47
+ const upsertDefaultSchemaFactoryConfig = (content, factoryName) => {
48
+ const hasExportDefaultObject = /export\s+default\s*\{[\s\S]*\}\s*;?/.test(content);
49
+ const defaultFactoryEntry = buildDefaultSchemaFactoryConfigEntry(factoryName);
50
+ const coreAliasEntry = `coreAlias: '$core'`;
51
+
52
+ if (!hasExportDefaultObject) {
53
+ return `export default {\n\t${coreAliasEntry},\n\t${defaultFactoryEntry}\n};\n`;
54
+ }
55
+
56
+ let nextContent = content;
57
+
58
+ if (/defaultSchemaFactory\s*:/.test(nextContent)) {
59
+ nextContent = nextContent.replace(/defaultSchemaFactory\s*:\s*(\{[\s\S]*?\n\s*\}|['"`][^'"`]+['"`])/m, defaultFactoryEntry);
60
+ } else if (/defaultSchema\s*:/.test(nextContent)) {
61
+ nextContent = nextContent.replace(/defaultSchema\s*:\s*(\{[\s\S]*?\n\s*\}|['"`][^'"`]+['"`])/m, defaultFactoryEntry);
62
+ } else {
63
+ nextContent = nextContent.replace(/export\s+default\s*\{([\s\S]*)\}\s*;?/, (_full, body) => {
64
+ const trimmedRight = body.replace(/\s*$/, '');
65
+ if (!trimmedRight.length) {
66
+ return `export default {\n\t${defaultFactoryEntry}\n};`;
67
+ }
68
+
69
+ const withComma = trimmedRight.endsWith(',') ? trimmedRight : `${trimmedRight},`;
70
+ return `export default {${withComma}\n\t${defaultFactoryEntry}\n};`;
71
+ });
72
+ }
73
+
74
+ if (/coreAlias\s*:/.test(nextContent)) {
75
+ return nextContent.replace(/coreAlias\s*:\s*['"`][^'"`]+['"`]/, coreAliasEntry);
76
+ }
77
+
78
+ return nextContent.replace(/export\s+default\s*\{([\s\S]*)\}\s*;?/, (_full, body) => {
79
+ const trimmedRight = body.replace(/\s*$/, '');
80
+ if (!trimmedRight.length) {
81
+ return `export default {\n\t${coreAliasEntry}\n};`;
82
+ }
83
+
84
+ const withComma = trimmedRight.endsWith(',') ? trimmedRight : `${trimmedRight},`;
85
+ return `export default {${withComma}\n\t${coreAliasEntry}\n};`;
86
+ });
87
+ };
88
+
89
+ export default async function createCoreSchema() {
90
+ const { schemaFactoryNameRaw } = await prompts({
91
+ type: 'text',
92
+ name: 'schemaFactoryNameRaw',
93
+ message: 'Schema factory name:',
94
+ initial: 'Schema'
95
+ });
96
+
97
+ const schemaFactoryName = toPascalCase(String(schemaFactoryNameRaw ?? 'Schema')) || 'Schema';
98
+
99
+ const { preset } = await prompts({
100
+ type: 'select',
101
+ name: 'preset',
102
+ message: 'Choose validation messages:',
103
+ choices: [
104
+ { title: 'Russian', value: 'ru' },
105
+ { title: 'English', value: 'en' },
106
+ { title: 'i18n', value: 'i18n' }
107
+ ],
108
+ initial: 2
109
+ });
110
+
111
+ const selectedPreset = preset ?? 'i18n';
112
+ const messagesImportName = resolvePresetImportName(selectedPreset);
113
+ const coreSchemaPath = path.join(process.cwd(), 'src', 'core', 'schema');
114
+ const customRulesPath = path.join(coreSchemaPath, 'custom-rules');
115
+ const filePath = path.join(coreSchemaPath, `${schemaFactoryName}.ts`);
116
+
117
+ await fs.mkdir(coreSchemaPath, { recursive: true });
118
+ await fs.mkdir(customRulesPath, { recursive: true });
119
+
120
+ const created = await writeIfNotExists(filePath, createSchemaFactoryTemplate(schemaFactoryName, messagesImportName));
121
+ if (!created) {
122
+ console.log(`⚠️ Schema factory "${schemaFactoryName}" already exists. File was not overwritten.`);
123
+ } else {
124
+ await updateIndexTs(coreSchemaPath);
125
+ console.log(`✅ Core schema factory created: ${filePath}`);
126
+ }
127
+
128
+ const { useAsDefault } = await prompts({
129
+ type: 'confirm',
130
+ name: 'useAsDefault',
131
+ message: 'Use this schema factory as default for the project?',
132
+ initial: true
133
+ });
134
+
135
+ if (!useAsDefault) return;
136
+
137
+ const config = resolveConfigFile(process.cwd());
138
+ const nextContent = upsertDefaultSchemaFactoryConfig(config.content, schemaFactoryName);
139
+ await fs.writeFile(config.filepath, nextContent, 'utf-8');
140
+ console.log(`✅ Default schema factory saved to ${path.basename(config.filepath)}`);
141
+ }
@@ -0,0 +1,142 @@
1
+ import fs from 'node:fs/promises';
2
+ import fsSync from 'node:fs';
3
+ import path from 'node:path';
4
+ import prompts from 'prompts';
5
+ import { writeIfNotExists, updateIndexTs } from '../utils/fileUtils.js';
6
+ import { toPascalCase } from '../utils/contextUtils.js';
7
+
8
+ type MessagePreset = 'ru' | 'en' | 'i18n';
9
+ type ConfigPath = 'ts' | 'js';
10
+
11
+ const resolvePresetImportName = (preset: MessagePreset): string => {
12
+ switch (preset) {
13
+ case 'ru':
14
+ return 'validationMessagesRu';
15
+ case 'en':
16
+ return 'validationMessagesEn';
17
+ case 'i18n':
18
+ return 'validationMessagesI18n';
19
+ }
20
+ };
21
+
22
+ const createSchemaFactoryTemplate = (
23
+ factoryName: string,
24
+ messagesImportName: string
25
+ ): string => `import { createSchemaFactory, createRules, ${messagesImportName} } from '@azure-net/kit/delivery';
26
+
27
+ export const ${factoryName} = createSchemaFactory(createRules(${messagesImportName}));
28
+ `;
29
+
30
+ const resolveConfigFile = (cwd: string): { filepath: string; type: ConfigPath; exists: boolean; content: string } => {
31
+ const tsPath = path.join(cwd, 'azure-net.config.ts');
32
+ const jsPath = path.join(cwd, 'azure-net.config.js');
33
+
34
+ if (fsSync.existsSync(tsPath)) {
35
+ return { filepath: tsPath, type: 'ts', exists: true, content: fsSync.readFileSync(tsPath, 'utf-8') };
36
+ }
37
+ if (fsSync.existsSync(jsPath)) {
38
+ return { filepath: jsPath, type: 'js', exists: true, content: fsSync.readFileSync(jsPath, 'utf-8') };
39
+ }
40
+
41
+ return { filepath: tsPath, type: 'ts', exists: false, content: '' };
42
+ };
43
+
44
+ const buildDefaultSchemaFactoryConfigEntry = (factoryName: string): string => {
45
+ return `defaultSchemaFactory: '${factoryName}'`;
46
+ };
47
+
48
+ const upsertDefaultSchemaFactoryConfig = (content: string, factoryName: string): string => {
49
+ const hasExportDefaultObject = /export\s+default\s*\{[\s\S]*\}\s*;?/.test(content);
50
+ const defaultFactoryEntry = buildDefaultSchemaFactoryConfigEntry(factoryName);
51
+ const coreAliasEntry = `coreAlias: '$core'`;
52
+
53
+ if (!hasExportDefaultObject) {
54
+ return `export default {\n\t${coreAliasEntry},\n\t${defaultFactoryEntry}\n};\n`;
55
+ }
56
+
57
+ let nextContent = content;
58
+
59
+ if (/defaultSchemaFactory\s*:/.test(nextContent)) {
60
+ nextContent = nextContent.replace(/defaultSchemaFactory\s*:\s*(\{[\s\S]*?\n\s*\}|['"`][^'"`]+['"`])/m, defaultFactoryEntry);
61
+ } else if (/defaultSchema\s*:/.test(nextContent)) {
62
+ nextContent = nextContent.replace(/defaultSchema\s*:\s*(\{[\s\S]*?\n\s*\}|['"`][^'"`]+['"`])/m, defaultFactoryEntry);
63
+ } else {
64
+ nextContent = nextContent.replace(/export\s+default\s*\{([\s\S]*)\}\s*;?/, (_full, body: string) => {
65
+ const trimmedRight = body.replace(/\s*$/, '');
66
+ if (!trimmedRight.length) {
67
+ return `export default {\n\t${defaultFactoryEntry}\n};`;
68
+ }
69
+
70
+ const withComma = trimmedRight.endsWith(',') ? trimmedRight : `${trimmedRight},`;
71
+ return `export default {${withComma}\n\t${defaultFactoryEntry}\n};`;
72
+ });
73
+ }
74
+
75
+ if (/coreAlias\s*:/.test(nextContent)) {
76
+ return nextContent.replace(/coreAlias\s*:\s*['"`][^'"`]+['"`]/, coreAliasEntry);
77
+ }
78
+
79
+ return nextContent.replace(/export\s+default\s*\{([\s\S]*)\}\s*;?/, (_full, body: string) => {
80
+ const trimmedRight = body.replace(/\s*$/, '');
81
+ if (!trimmedRight.length) {
82
+ return `export default {\n\t${coreAliasEntry}\n};`;
83
+ }
84
+
85
+ const withComma = trimmedRight.endsWith(',') ? trimmedRight : `${trimmedRight},`;
86
+ return `export default {${withComma}\n\t${coreAliasEntry}\n};`;
87
+ });
88
+ };
89
+
90
+ export default async function createCoreSchema(): Promise<void> {
91
+ const { schemaFactoryNameRaw } = await prompts({
92
+ type: 'text',
93
+ name: 'schemaFactoryNameRaw',
94
+ message: 'Schema factory name:',
95
+ initial: 'Schema'
96
+ });
97
+
98
+ const schemaFactoryName = toPascalCase(String(schemaFactoryNameRaw ?? 'Schema')) || 'Schema';
99
+
100
+ const { preset } = await prompts({
101
+ type: 'select',
102
+ name: 'preset',
103
+ message: 'Choose validation messages:',
104
+ choices: [
105
+ { title: 'Russian', value: 'ru' },
106
+ { title: 'English', value: 'en' },
107
+ { title: 'i18n', value: 'i18n' }
108
+ ],
109
+ initial: 2
110
+ });
111
+
112
+ const selectedPreset = (preset ?? 'i18n') as MessagePreset;
113
+ const messagesImportName = resolvePresetImportName(selectedPreset);
114
+ const coreSchemaPath = path.join(process.cwd(), 'src', 'core', 'schema');
115
+ const customRulesPath = path.join(coreSchemaPath, 'custom-rules');
116
+ const filePath = path.join(coreSchemaPath, `${schemaFactoryName}.ts`);
117
+
118
+ await fs.mkdir(coreSchemaPath, { recursive: true });
119
+ await fs.mkdir(customRulesPath, { recursive: true });
120
+
121
+ const created = await writeIfNotExists(filePath, createSchemaFactoryTemplate(schemaFactoryName, messagesImportName));
122
+ if (!created) {
123
+ console.log(`⚠️ Schema factory "${schemaFactoryName}" already exists. File was not overwritten.`);
124
+ } else {
125
+ await updateIndexTs(coreSchemaPath);
126
+ console.log(`✅ Core schema factory created: ${filePath}`);
127
+ }
128
+
129
+ const { useAsDefault } = await prompts({
130
+ type: 'confirm',
131
+ name: 'useAsDefault',
132
+ message: 'Use this schema factory as default for the project?',
133
+ initial: true
134
+ });
135
+
136
+ if (!useAsDefault) return;
137
+
138
+ const config = resolveConfigFile(process.cwd());
139
+ const nextContent = upsertDefaultSchemaFactoryConfig(config.content, schemaFactoryName);
140
+ await fs.writeFile(config.filepath, nextContent, 'utf-8');
141
+ console.log(`✅ Default schema factory saved to ${path.basename(config.filepath)}`);
142
+ }
@@ -0,0 +1,173 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import prompts from 'prompts';
4
+ import { writeIfNotExists, updateIndexTs } from '../utils/fileUtils.js';
5
+ import { toPascalCase } from '../utils/contextUtils.js';
6
+ import { loadUserConfig, resolveConfigFile } from '../utils/loadConfig.js';
7
+ import { normalizeContexts } from '../init/initAliases.js';
8
+
9
+ const createDatasourceTemplateWithoutResponse = (
10
+ className
11
+ ) => `import { BaseHttpDatasource, type CreateRequestCallbackType } from '@azure-net/kit/infra';
12
+
13
+ export class ${className} extends BaseHttpDatasource {
14
+ \tasync createRequest<T>(callback: CreateRequestCallbackType<T>) {
15
+ \t\treturn await this.createRawRequest<T>(callback);
16
+ \t}
17
+ }
18
+ `;
19
+
20
+ const createDatasourceTemplateWithResponseAndInterface = (
21
+ className,
22
+ responseName,
23
+ coreAlias
24
+ ) => `import { BaseHttpDatasource, type CreateRequestCallbackType } from '@azure-net/kit/infra';
25
+ import { ${responseName}, type I${responseName} } from '${coreAlias}/response';
26
+
27
+ export class ${className} extends BaseHttpDatasource {
28
+ \tasync createRequest<T>(callback: CreateRequestCallbackType<I${responseName}<T>>) {
29
+ \t\treturn new ${responseName}<T>(await this.createRawRequest<I${responseName}<T>>(callback));
30
+ \t}
31
+ }
32
+ `;
33
+
34
+ const createDatasourceTemplateWithResponse = (
35
+ className,
36
+ responseName,
37
+ coreAlias
38
+ ) => `import { BaseHttpDatasource, type CreateRequestCallbackType } from '@azure-net/kit/infra';
39
+ import { ${responseName} } from '${coreAlias}/response';
40
+
41
+ export class ${className} extends BaseHttpDatasource {
42
+ \tasync createRequest<T>(callback: CreateRequestCallbackType<T>) {
43
+ \t\treturn new ${responseName}<T>(await this.createRawRequest<T>(callback));
44
+ \t}
45
+ }
46
+ `;
47
+
48
+ const ensureDatasourceClassName = (rawName) => {
49
+ const normalized = toPascalCase(rawName || 'MockApi');
50
+ return normalized.endsWith('Datasource') ? normalized : `${normalized}Datasource`;
51
+ };
52
+
53
+ const getContextChoices = async () => {
54
+ const config = await loadUserConfig();
55
+ const contexts = normalizeContexts(config.contexts);
56
+
57
+ const choices = [{ title: 'core', value: 'core' }];
58
+ for (const context of contexts) {
59
+ choices.push({ title: context.name, value: context.name });
60
+ }
61
+
62
+ return choices;
63
+ };
64
+
65
+ const getCoreResponses = async () => {
66
+ const responsesPath = path.join(process.cwd(), 'src', 'core', 'response');
67
+ try {
68
+ const files = await fs.readdir(responsesPath);
69
+ return files.filter((file) => file.endsWith('.ts') && file !== 'index.ts').map((file) => file.replace(/\.ts$/, ''));
70
+ } catch {
71
+ return [];
72
+ }
73
+ };
74
+
75
+ const hasResponseInterface = async (responseName) => {
76
+ const responsePath = path.join(process.cwd(), 'src', 'core', 'response', `${responseName}.ts`);
77
+ try {
78
+ const content = await fs.readFile(responsePath, 'utf-8');
79
+ const interfaceRegex = new RegExp(`export\\s+interface\\s+I${responseName}\\b`);
80
+ return interfaceRegex.test(content);
81
+ } catch {
82
+ return false;
83
+ }
84
+ };
85
+
86
+ const getConfiguredCoreAliasOrThrow = async () => {
87
+ const configRef = resolveConfigFile();
88
+ if (!configRef.exists) {
89
+ throw new Error(
90
+ 'azure-net.config is not configured. Run "azure-net init folders-structure" and do not forget to fill contexts if you need them.'
91
+ );
92
+ }
93
+
94
+ const content = await fs.readFile(configRef.filepath, 'utf-8');
95
+ const aliasMatch = content.match(/coreAlias\s*:\s*['"`]([^'"`]+)['"`]/);
96
+ const alias = aliasMatch?.[1]?.trim();
97
+
98
+ if (!alias) {
99
+ throw new Error(
100
+ 'azure-net.config is not configured. Run "azure-net init folders-structure" and do not forget to fill contexts if you need them.'
101
+ );
102
+ }
103
+
104
+ return alias.startsWith('$') ? alias : `$${alias}`;
105
+ };
106
+
107
+ const resolveTargetPath = (target) => {
108
+ if (target === 'core') {
109
+ return path.join(process.cwd(), 'src', 'core', 'datasource');
110
+ }
111
+
112
+ return path.join(process.cwd(), 'src', 'app', target, 'layers', 'infrastructure', 'http', 'datasources');
113
+ };
114
+
115
+ export default async function createDatasource() {
116
+ const { datasourceNameRaw } = await prompts({
117
+ type: 'text',
118
+ name: 'datasourceNameRaw',
119
+ message: 'Datasource name:',
120
+ initial: 'MockApi'
121
+ });
122
+
123
+ const className = ensureDatasourceClassName(String(datasourceNameRaw ?? 'MockApi'));
124
+
125
+ const contextChoices = await getContextChoices();
126
+ const { target } = await prompts({
127
+ type: 'select',
128
+ name: 'target',
129
+ message: 'Where to create datasource?',
130
+ choices: contextChoices,
131
+ initial: 0
132
+ });
133
+
134
+ const selectedTarget = String(target ?? 'core');
135
+ const coreResponses = await getCoreResponses();
136
+
137
+ let selectedResponse = null;
138
+ if (coreResponses.length > 0) {
139
+ const { responseName } = await prompts({
140
+ type: 'select',
141
+ name: 'responseName',
142
+ message: 'Select response for datasource:',
143
+ choices: [{ title: 'No response', value: null }, ...coreResponses.map((response) => ({ title: response, value: response }))],
144
+ initial: 0
145
+ });
146
+ selectedResponse = responseName ? String(responseName) : null;
147
+ }
148
+
149
+ let content;
150
+ if (!selectedResponse) {
151
+ content = createDatasourceTemplateWithoutResponse(className);
152
+ } else {
153
+ const coreAlias = await getConfiguredCoreAliasOrThrow();
154
+ const withInterface = await hasResponseInterface(selectedResponse);
155
+ content = withInterface
156
+ ? createDatasourceTemplateWithResponseAndInterface(className, selectedResponse, coreAlias)
157
+ : createDatasourceTemplateWithResponse(className, selectedResponse, coreAlias);
158
+ }
159
+
160
+ const targetPath = resolveTargetPath(selectedTarget);
161
+ await fs.mkdir(targetPath, { recursive: true });
162
+
163
+ const filePath = path.join(targetPath, `${className}.ts`);
164
+ const created = await writeIfNotExists(filePath, content);
165
+ await updateIndexTs(targetPath);
166
+
167
+ if (!created) {
168
+ console.log(`⚠️ Datasource "${className}" already exists. File was not overwritten.`);
169
+ return;
170
+ }
171
+
172
+ console.log(`✅ Datasource created: ${filePath}`);
173
+ }
@@ -0,0 +1,175 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import prompts from 'prompts';
4
+ import { writeIfNotExists, updateIndexTs } from '../utils/fileUtils.js';
5
+ import { toPascalCase } from '../utils/contextUtils.js';
6
+ import { loadUserConfig, resolveConfigFile } from '../utils/loadConfig.js';
7
+ import { normalizeContexts } from '../init/initAliases.js';
8
+
9
+ type ContextChoice = { title: string; value: string };
10
+
11
+ const createDatasourceTemplateWithoutResponse = (
12
+ className: string
13
+ ): string => `import { BaseHttpDatasource, type CreateRequestCallbackType } from '@azure-net/kit/infra';
14
+
15
+ export class ${className} extends BaseHttpDatasource {
16
+ \tasync createRequest<T>(callback: CreateRequestCallbackType<T>) {
17
+ \t\treturn await this.createRawRequest<T>(callback);
18
+ \t}
19
+ }
20
+ `;
21
+
22
+ const createDatasourceTemplateWithResponseAndInterface = (
23
+ className: string,
24
+ responseName: string,
25
+ coreAlias: string
26
+ ): string => `import { BaseHttpDatasource, type CreateRequestCallbackType } from '@azure-net/kit/infra';
27
+ import { ${responseName}, type I${responseName} } from '${coreAlias}/response';
28
+
29
+ export class ${className} extends BaseHttpDatasource {
30
+ \tasync createRequest<T>(callback: CreateRequestCallbackType<I${responseName}<T>>) {
31
+ \t\treturn new ${responseName}<T>(await this.createRawRequest<I${responseName}<T>>(callback));
32
+ \t}
33
+ }
34
+ `;
35
+
36
+ const createDatasourceTemplateWithResponse = (
37
+ className: string,
38
+ responseName: string,
39
+ coreAlias: string
40
+ ): string => `import { BaseHttpDatasource, type CreateRequestCallbackType } from '@azure-net/kit/infra';
41
+ import { ${responseName} } from '${coreAlias}/response';
42
+
43
+ export class ${className} extends BaseHttpDatasource {
44
+ \tasync createRequest<T>(callback: CreateRequestCallbackType<T>) {
45
+ \t\treturn new ${responseName}<T>(await this.createRawRequest<T>(callback));
46
+ \t}
47
+ }
48
+ `;
49
+
50
+ const ensureDatasourceClassName = (rawName: string): string => {
51
+ const normalized = toPascalCase(rawName || 'MockApi');
52
+ return normalized.endsWith('Datasource') ? normalized : `${normalized}Datasource`;
53
+ };
54
+
55
+ const getContextChoices = async (): Promise<ContextChoice[]> => {
56
+ const config = await loadUserConfig();
57
+ const contexts = normalizeContexts(config.contexts);
58
+
59
+ const choices: ContextChoice[] = [{ title: 'core', value: 'core' }];
60
+ for (const context of contexts) {
61
+ choices.push({ title: context.name, value: context.name });
62
+ }
63
+
64
+ return choices;
65
+ };
66
+
67
+ const getCoreResponses = async (): Promise<string[]> => {
68
+ const responsesPath = path.join(process.cwd(), 'src', 'core', 'response');
69
+ try {
70
+ const files = await fs.readdir(responsesPath);
71
+ return files.filter((file) => file.endsWith('.ts') && file !== 'index.ts').map((file) => file.replace(/\.ts$/, ''));
72
+ } catch {
73
+ return [];
74
+ }
75
+ };
76
+
77
+ const hasResponseInterface = async (responseName: string): Promise<boolean> => {
78
+ const responsePath = path.join(process.cwd(), 'src', 'core', 'response', `${responseName}.ts`);
79
+ try {
80
+ const content = await fs.readFile(responsePath, 'utf-8');
81
+ const interfaceRegex = new RegExp(`export\\s+interface\\s+I${responseName}\\b`);
82
+ return interfaceRegex.test(content);
83
+ } catch {
84
+ return false;
85
+ }
86
+ };
87
+
88
+ const getConfiguredCoreAliasOrThrow = async (): Promise<string> => {
89
+ const configRef = resolveConfigFile();
90
+ if (!configRef.exists) {
91
+ throw new Error(
92
+ 'azure-net.config is not configured. Run "azure-net init folders-structure" and do not forget to fill contexts if you need them.'
93
+ );
94
+ }
95
+
96
+ const content = await fs.readFile(configRef.filepath, 'utf-8');
97
+ const aliasMatch = content.match(/coreAlias\s*:\s*['"`]([^'"`]+)['"`]/);
98
+ const alias = aliasMatch?.[1]?.trim();
99
+
100
+ if (!alias) {
101
+ throw new Error(
102
+ 'azure-net.config is not configured. Run "azure-net init folders-structure" and do not forget to fill contexts if you need them.'
103
+ );
104
+ }
105
+
106
+ return alias.startsWith('$') ? alias : `$${alias}`;
107
+ };
108
+
109
+ const resolveTargetPath = (target: string): string => {
110
+ if (target === 'core') {
111
+ return path.join(process.cwd(), 'src', 'core', 'datasource');
112
+ }
113
+
114
+ return path.join(process.cwd(), 'src', 'app', target, 'layers', 'infrastructure', 'http', 'datasources');
115
+ };
116
+
117
+ export default async function createDatasource(): Promise<void> {
118
+ const { datasourceNameRaw } = await prompts({
119
+ type: 'text',
120
+ name: 'datasourceNameRaw',
121
+ message: 'Datasource name:',
122
+ initial: 'MockApi'
123
+ });
124
+
125
+ const className = ensureDatasourceClassName(String(datasourceNameRaw ?? 'MockApi'));
126
+
127
+ const contextChoices = await getContextChoices();
128
+ const { target } = await prompts({
129
+ type: 'select',
130
+ name: 'target',
131
+ message: 'Where to create datasource?',
132
+ choices: contextChoices,
133
+ initial: 0
134
+ });
135
+
136
+ const selectedTarget = String(target ?? 'core');
137
+ const coreResponses = await getCoreResponses();
138
+
139
+ let selectedResponse: string | null = null;
140
+ if (coreResponses.length > 0) {
141
+ const { responseName } = await prompts({
142
+ type: 'select',
143
+ name: 'responseName',
144
+ message: 'Select response for datasource:',
145
+ choices: [{ title: 'No response', value: null }, ...coreResponses.map((response) => ({ title: response, value: response }))],
146
+ initial: 0
147
+ });
148
+ selectedResponse = responseName ? String(responseName) : null;
149
+ }
150
+
151
+ let content: string;
152
+ if (!selectedResponse) {
153
+ content = createDatasourceTemplateWithoutResponse(className);
154
+ } else {
155
+ const coreAlias = await getConfiguredCoreAliasOrThrow();
156
+ const withInterface = await hasResponseInterface(selectedResponse);
157
+ content = withInterface
158
+ ? createDatasourceTemplateWithResponseAndInterface(className, selectedResponse, coreAlias)
159
+ : createDatasourceTemplateWithResponse(className, selectedResponse, coreAlias);
160
+ }
161
+
162
+ const targetPath = resolveTargetPath(selectedTarget);
163
+ await fs.mkdir(targetPath, { recursive: true });
164
+
165
+ const filePath = path.join(targetPath, `${className}.ts`);
166
+ const created = await writeIfNotExists(filePath, content);
167
+ await updateIndexTs(targetPath);
168
+
169
+ if (!created) {
170
+ console.log(`⚠️ Datasource "${className}" already exists. File was not overwritten.`);
171
+ return;
172
+ }
173
+
174
+ console.log(`✅ Datasource created: ${filePath}`);
175
+ }