nestcraftx 0.3.0 → 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/.github/workflows/ci.yml +35 -0
- package/CHANGELOG.fr.md +74 -0
- package/CHANGELOG.md +74 -0
- package/PROGRESS.md +70 -67
- package/README.fr.md +60 -69
- package/bin/nestcraft.js +8 -0
- package/commands/demo.js +12 -46
- package/commands/generate.js +551 -2
- package/commands/generateConf.js +4 -4
- package/commands/help.js +11 -0
- package/commands/info.js +2 -3
- package/commands/list.js +93 -0
- package/commands/new.js +28 -56
- package/jest.config.js +9 -0
- package/package.json +7 -2
- package/readme.md +59 -68
- package/tests/e2e/generator.spec.js +71 -0
- package/tests/unit/appModuleUpdater.spec.js +111 -0
- package/tests/unit/cliParser.spec.js +74 -0
- package/tests/unit/controllerGenerator.spec.js +145 -0
- package/tests/unit/dtoGenerator.spec.js +87 -0
- package/tests/unit/entityGenerator.spec.js +65 -0
- package/tests/unit/generateCommand.spec.js +100 -0
- package/tests/unit/health.spec.js +69 -0
- package/tests/unit/listCommand.spec.js +91 -0
- package/tests/unit/middlewareGenerator.spec.js +64 -0
- package/tests/unit/newCommand.spec.js +88 -0
- package/tests/unit/relationCommand.spec.js +202 -0
- package/tests/unit/throttler.spec.js +117 -0
- package/utils/app-module.updater.js +6 -0
- package/utils/configs/setupCleanArchitecture.js +1 -1
- package/utils/configs/setupLightArchitecture.js +98 -26
- package/utils/envGenerator.js +9 -1
- package/utils/file-system.js +15 -0
- package/utils/file-utils/packageJsonUtils.js +6 -0
- package/utils/file-utils/saveProjectConfig.js +9 -0
- package/utils/fullModeInput.js +12 -229
- package/utils/generators/application/dtoGenerator.js +26 -8
- package/utils/generators/application/dtoUpdater.js +5 -0
- package/utils/generators/cleanModuleGenerator.js +40 -11
- package/utils/generators/domain/entityUpdater.js +5 -0
- package/utils/generators/infrastructure/mapperUpdater.js +5 -0
- package/utils/generators/infrastructure/middlewareGenerator.js +112 -315
- package/utils/generators/infrastructure/mongooseSchemaGenerator.js +114 -4
- package/utils/generators/infrastructure/repositoryGenerator.js +114 -14
- package/utils/generators/lightModuleGenerator.js +14 -0
- package/utils/generators/presentation/controllerGenerator.js +70 -19
- package/utils/helpers.js +8 -2
- package/utils/interactive/askEntityInputs.js +5 -68
- package/utils/interactive/askRelationCommand.js +98 -0
- package/utils/interactive/entityBuilder.js +411 -0
- package/utils/lightModeInput.js +22 -241
- package/utils/setups/orms/typeOrmSetup.js +42 -8
- package/utils/setups/projectSetup.js +88 -10
- package/utils/setups/setupAuth.js +335 -20
- package/utils/setups/setupDatabase.js +4 -1
- package/utils/setups/setupHealth.js +74 -0
- package/utils/setups/setupMongoose.js +84 -32
- package/utils/setups/setupPrisma.js +151 -4
- package/utils/setups/setupSwagger.js +15 -8
- package/utils/setups/setupThrottler.js +92 -0
- package/utils/shell.js +61 -9
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const listCommand = require("../../commands/list");
|
|
4
|
+
const { logError } = require("../../utils/loggers/logError");
|
|
5
|
+
const { logWarning } = require("../../utils/loggers/logWarning");
|
|
6
|
+
|
|
7
|
+
jest.mock("fs");
|
|
8
|
+
jest.mock("../../utils/loggers/logError");
|
|
9
|
+
jest.mock("../../utils/loggers/logWarning");
|
|
10
|
+
jest.mock("../../utils/loggers/logInfo");
|
|
11
|
+
|
|
12
|
+
describe("listCommand - nestcraftx list", () => {
|
|
13
|
+
let logSpy;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
jest.clearAllMocks();
|
|
17
|
+
logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
logSpy.mockRestore();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should print an error if .nestcraftxrc does not exist", async () => {
|
|
25
|
+
fs.existsSync.mockReturnValue(false);
|
|
26
|
+
|
|
27
|
+
await listCommand();
|
|
28
|
+
|
|
29
|
+
expect(logError).toHaveBeenCalledWith(
|
|
30
|
+
expect.stringContaining("Aucun fichier de configuration NestcraftX trouvé.")
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should print a warning if there are no entities", async () => {
|
|
35
|
+
fs.existsSync.mockReturnValue(true);
|
|
36
|
+
fs.readFileSync.mockReturnValue(
|
|
37
|
+
JSON.stringify({
|
|
38
|
+
name: "test-app",
|
|
39
|
+
mode: "full",
|
|
40
|
+
orm: "prisma",
|
|
41
|
+
database: "postgresql",
|
|
42
|
+
entities: [],
|
|
43
|
+
relations: []
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
await listCommand();
|
|
48
|
+
|
|
49
|
+
expect(logWarning).toHaveBeenCalledWith(
|
|
50
|
+
expect.stringContaining("Aucune entité configurée pour le moment.")
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should print a beautiful table if entities and relations exist", async () => {
|
|
55
|
+
fs.existsSync.mockReturnValue(true);
|
|
56
|
+
fs.readFileSync.mockReturnValue(
|
|
57
|
+
JSON.stringify({
|
|
58
|
+
name: "test-app",
|
|
59
|
+
mode: "full",
|
|
60
|
+
orm: "prisma",
|
|
61
|
+
database: "postgresql",
|
|
62
|
+
entities: [
|
|
63
|
+
{
|
|
64
|
+
name: "user",
|
|
65
|
+
fields: [
|
|
66
|
+
{ name: "email", type: "string" },
|
|
67
|
+
{ name: "password", type: "string" }
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
relations: [
|
|
72
|
+
{
|
|
73
|
+
from: "user",
|
|
74
|
+
to: "session",
|
|
75
|
+
type: "1-n"
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
})
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
await listCommand();
|
|
82
|
+
|
|
83
|
+
// Verify console.log prints the table structure
|
|
84
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("Project:"));
|
|
85
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("ENTITIES"));
|
|
86
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("user"));
|
|
87
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("email (string)"));
|
|
88
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("RELATIONSHIPS"));
|
|
89
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("user ──► session"));
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const { generateMiddlewares } = require('../../utils/generators/infrastructure/middlewareGenerator');
|
|
2
|
+
const { createFile, updateFile } = require('../../utils/file-system');
|
|
3
|
+
|
|
4
|
+
// Mock file-system
|
|
5
|
+
jest.mock('../../utils/file-system', () => ({
|
|
6
|
+
createDirectory: jest.fn().mockResolvedValue(undefined),
|
|
7
|
+
createFile: jest.fn().mockResolvedValue(undefined),
|
|
8
|
+
updateFile: jest.fn().mockResolvedValue(undefined),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
describe('middlewareGenerator - generateMiddlewares', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
jest.clearAllMocks();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should generate typed exception filter for prisma ORM', async () => {
|
|
17
|
+
await generateMiddlewares('prisma');
|
|
18
|
+
|
|
19
|
+
// Vérifier la création de all-exceptions.filter.ts
|
|
20
|
+
const filterCall = createFile.mock.calls.find(call =>
|
|
21
|
+
call[0].path.endsWith('all-exceptions.filter.ts')
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
expect(filterCall).toBeDefined();
|
|
25
|
+
const content = filterCall[0].contente;
|
|
26
|
+
|
|
27
|
+
expect(content).toContain('export interface ErrorResponse');
|
|
28
|
+
expect(content).toContain('AllExceptionsFilter implements ExceptionFilter');
|
|
29
|
+
expect(content).toContain('PrismaClientKnownRequestError');
|
|
30
|
+
expect(content).toContain('process.env.NODE_ENV === \'production\'');
|
|
31
|
+
expect(content).toContain('payload: ErrorResponse');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should generate typed exception filter for mongoose ORM', async () => {
|
|
35
|
+
await generateMiddlewares('mongoose');
|
|
36
|
+
|
|
37
|
+
const filterCall = createFile.mock.calls.find(call =>
|
|
38
|
+
call[0].path.endsWith('all-exceptions.filter.ts')
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
expect(filterCall).toBeDefined();
|
|
42
|
+
const content = filterCall[0].contente;
|
|
43
|
+
|
|
44
|
+
expect(content).toContain('export interface ErrorResponse');
|
|
45
|
+
expect(content).toContain('MongoError');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should generate fallback/universal typed exception filter', async () => {
|
|
49
|
+
await generateMiddlewares('global');
|
|
50
|
+
|
|
51
|
+
const filterCall = createFile.mock.calls.find(call =>
|
|
52
|
+
call[0].path.endsWith('all-exceptions.filter.ts')
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
expect(filterCall).toBeDefined();
|
|
56
|
+
const content = filterCall[0].contente;
|
|
57
|
+
|
|
58
|
+
expect(content).toContain('export interface ErrorResponse');
|
|
59
|
+
// Doit contenir les vérifications spécifiques des différents ORM pour la version universelle
|
|
60
|
+
expect(content).toContain('PrismaClientKnownRequestError');
|
|
61
|
+
expect(content).toContain('MongoError');
|
|
62
|
+
expect(content).toContain('QueryFailedError');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const { getFullModeInputs } = require("../../utils/fullModeInput");
|
|
2
|
+
const { getLightModeInputs } = require("../../utils/lightModeInput");
|
|
3
|
+
const { runProjectSetupPipeline } = require("../../utils/setups/projectSetup");
|
|
4
|
+
const readline = require("readline-sync");
|
|
5
|
+
|
|
6
|
+
// Mock dependencies at the top before requiring commands/new
|
|
7
|
+
jest.mock("../../utils/fullModeInput", () => ({
|
|
8
|
+
getFullModeInputs: jest.fn()
|
|
9
|
+
}));
|
|
10
|
+
jest.mock("../../utils/lightModeInput", () => ({
|
|
11
|
+
getLightModeInputs: jest.fn()
|
|
12
|
+
}));
|
|
13
|
+
jest.mock("../../utils/setups/projectSetup");
|
|
14
|
+
jest.mock("readline-sync");
|
|
15
|
+
jest.mock("../../utils/loggers/logSuccess");
|
|
16
|
+
jest.mock("../../utils/loggers/logInfo");
|
|
17
|
+
jest.mock("../../utils/loggers/logWarning");
|
|
18
|
+
|
|
19
|
+
const newCommand = require("../../commands/new");
|
|
20
|
+
|
|
21
|
+
describe("newCommand - Interactive vs Non-Interactive Mode", () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
jest.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should run non-interactively in full mode if --orm and --full are passed", async () => {
|
|
27
|
+
runProjectSetupPipeline.mockResolvedValue();
|
|
28
|
+
|
|
29
|
+
const flags = { orm: "prisma", full: true, docker: false, auth: false, swagger: false };
|
|
30
|
+
await newCommand("test-non-interactive", flags);
|
|
31
|
+
|
|
32
|
+
expect(runProjectSetupPipeline).toHaveBeenCalledWith(
|
|
33
|
+
expect.objectContaining({
|
|
34
|
+
projectName: "test-non-interactive",
|
|
35
|
+
mode: "full",
|
|
36
|
+
useAuth: false,
|
|
37
|
+
useSwagger: false,
|
|
38
|
+
useDocker: false,
|
|
39
|
+
packageManager: "npm",
|
|
40
|
+
dbConfig: expect.objectContaining({
|
|
41
|
+
orm: "prisma"
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should run non-interactively in light mode if --orm and --light are passed", async () => {
|
|
48
|
+
runProjectSetupPipeline.mockResolvedValue();
|
|
49
|
+
|
|
50
|
+
const flags = { orm: "mongoose", light: true, auth: true, swagger: true };
|
|
51
|
+
await newCommand("test-light-non-interactive", flags);
|
|
52
|
+
|
|
53
|
+
expect(runProjectSetupPipeline).toHaveBeenCalledWith(
|
|
54
|
+
expect.objectContaining({
|
|
55
|
+
projectName: "test-light-non-interactive",
|
|
56
|
+
mode: "light",
|
|
57
|
+
useAuth: true,
|
|
58
|
+
useSwagger: true,
|
|
59
|
+
useDocker: true, // defaults to true
|
|
60
|
+
packageManager: "npm",
|
|
61
|
+
dbConfig: expect.objectContaining({
|
|
62
|
+
orm: "mongoose"
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should prompt if mandatory flags are missing", async () => {
|
|
69
|
+
getFullModeInputs.mockResolvedValue({
|
|
70
|
+
projectName: "test-app",
|
|
71
|
+
mode: "full",
|
|
72
|
+
useAuth: false,
|
|
73
|
+
useSwagger: false,
|
|
74
|
+
useDocker: true,
|
|
75
|
+
packageManager: "npm",
|
|
76
|
+
entitiesData: { entities: [], relations: [] },
|
|
77
|
+
selectedDB: "postgresql",
|
|
78
|
+
dbConfig: { orm: "prisma" }
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
runProjectSetupPipeline.mockResolvedValue();
|
|
82
|
+
|
|
83
|
+
// No mode/light/full flags, only orm -> should fall back to interactive
|
|
84
|
+
await newCommand("test-app", { orm: "prisma" });
|
|
85
|
+
|
|
86
|
+
expect(getFullModeInputs).toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// tests/unit/relationCommand.spec.js
|
|
2
|
+
//
|
|
3
|
+
// Tests unitaires pour la commande `nestcraftx g relation` (FEAT-14).
|
|
4
|
+
// Valide :
|
|
5
|
+
// - askRelationCommand : validation idempotence, entités insuffisantes
|
|
6
|
+
// - handleRelationGeneration : orchestration correcte (mocks)
|
|
7
|
+
// - addPrismaRelation : génération des bons champs selon le type de relation
|
|
8
|
+
|
|
9
|
+
const inquirer = require('inquirer');
|
|
10
|
+
|
|
11
|
+
// Mock inquirer globalement avant d'importer askRelationCommand
|
|
12
|
+
jest.mock('inquirer', () => {
|
|
13
|
+
const mockPrompt = jest.fn();
|
|
14
|
+
return {
|
|
15
|
+
prompt: mockPrompt,
|
|
16
|
+
default: {
|
|
17
|
+
prompt: mockPrompt,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const { askRelationCommand } = require('../../utils/interactive/askRelationCommand');
|
|
23
|
+
const { resolveForeignKeyOwner } = require('../../utils/generators/relation/relation.engine');
|
|
24
|
+
|
|
25
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
function makeConfig({ entities = [], relations = [] } = {}) {
|
|
28
|
+
return {
|
|
29
|
+
mode: 'full',
|
|
30
|
+
orm: 'prisma',
|
|
31
|
+
swagger: false,
|
|
32
|
+
auth: false,
|
|
33
|
+
entities,
|
|
34
|
+
relations,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Tests : askRelationCommand ──────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
describe('askRelationCommand', () => {
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
jest.clearAllMocks();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should return null and log error if fewer than 2 entities exist', async () => {
|
|
46
|
+
const config = makeConfig({ entities: [{ name: 'Post' }] });
|
|
47
|
+
|
|
48
|
+
// Intercepter logError sans faire crasher le test
|
|
49
|
+
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
50
|
+
|
|
51
|
+
const result = await askRelationCommand(config);
|
|
52
|
+
|
|
53
|
+
expect(result).toBeNull();
|
|
54
|
+
errorSpy.mockRestore();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should return null and log warning if relation already exists (idempotence)', async () => {
|
|
58
|
+
const config = makeConfig({
|
|
59
|
+
entities: [{ name: 'Post' }, { name: 'User' }],
|
|
60
|
+
relations: [{ from: 'post', to: 'user', type: 'n-1' }],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const mockPrompt = inquirer.prompt;
|
|
64
|
+
mockPrompt
|
|
65
|
+
.mockResolvedValueOnce({ source: 'Post' })
|
|
66
|
+
.mockResolvedValueOnce({ target: 'User' })
|
|
67
|
+
.mockResolvedValueOnce({ type: 'n-1' });
|
|
68
|
+
|
|
69
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
70
|
+
|
|
71
|
+
const result = await askRelationCommand(config);
|
|
72
|
+
|
|
73
|
+
// Relation already exists — should be blocked
|
|
74
|
+
expect(result).toBeNull();
|
|
75
|
+
warnSpy.mockRestore();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should return relation data when valid inputs are provided', async () => {
|
|
79
|
+
const config = makeConfig({
|
|
80
|
+
entities: [{ name: 'Post' }, { name: 'User' }],
|
|
81
|
+
relations: [], // Aucune relation existante
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const mockPrompt = inquirer.prompt;
|
|
85
|
+
mockPrompt
|
|
86
|
+
.mockResolvedValueOnce({ source: 'Post' })
|
|
87
|
+
.mockResolvedValueOnce({ target: 'User' })
|
|
88
|
+
.mockResolvedValueOnce({ type: 'n-1' });
|
|
89
|
+
|
|
90
|
+
const result = await askRelationCommand(config);
|
|
91
|
+
|
|
92
|
+
expect(result).toEqual({
|
|
93
|
+
source: 'post',
|
|
94
|
+
target: 'user',
|
|
95
|
+
type: 'n-1',
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ─── Tests : resolveForeignKeyOwner ─────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
describe('relation.engine - resolveForeignKeyOwner', () => {
|
|
103
|
+
it('should return null for 1-n (FK on the target side)', () => {
|
|
104
|
+
const result = resolveForeignKeyOwner('Post', 'User', '1-n');
|
|
105
|
+
expect(result).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should return owner for n-1 (FK on the source side)', () => {
|
|
109
|
+
const result = resolveForeignKeyOwner('Post', 'User', 'n-1');
|
|
110
|
+
expect(result).not.toBeNull();
|
|
111
|
+
expect(result.ownerEntity).toBe('User');
|
|
112
|
+
expect(result.relatedEntity).toBe('Post');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should return owner for 1-1 (FK on source by convention)', () => {
|
|
116
|
+
const result = resolveForeignKeyOwner('Post', 'User', '1-1');
|
|
117
|
+
expect(result).not.toBeNull();
|
|
118
|
+
expect(result.ownerEntity).toBe('User');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should return null for n-n (pivot table, no direct FK)', () => {
|
|
122
|
+
const result = resolveForeignKeyOwner('Post', 'Tag', 'n-n');
|
|
123
|
+
expect(result).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should throw for unknown relation type', () => {
|
|
127
|
+
expect(() => resolveForeignKeyOwner('Post', 'User', 'x-y')).toThrow('Unknown relation type');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ─── Tests : addPrismaRelation (génération de champs) ───────────────────────
|
|
132
|
+
|
|
133
|
+
describe('addPrismaRelation - field generation logic', () => {
|
|
134
|
+
function resolveSourceFields(source, target, type) {
|
|
135
|
+
const targetLow = target.toLowerCase();
|
|
136
|
+
const targetCap = target.charAt(0).toUpperCase() + target.slice(1);
|
|
137
|
+
switch (type) {
|
|
138
|
+
case 'n-1': return `${targetLow} ${targetCap} @relation(fields: [${targetLow}Id], references: [id])\n ${targetLow}Id String`;
|
|
139
|
+
case '1-n': return `${targetLow}s ${targetCap}[]`;
|
|
140
|
+
case '1-1': return `${targetLow} ${targetCap}? @relation(fields: [${targetLow}Id], references: [id])\n ${targetLow}Id String? @unique`;
|
|
141
|
+
case 'n-n': return `${targetLow}s ${targetCap}[]`;
|
|
142
|
+
default: throw new Error(`Unknown relation type: ${type}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
it('n-1: source should get a FK field and @relation decorator', () => {
|
|
147
|
+
const fields = resolveSourceFields('post', 'user', 'n-1');
|
|
148
|
+
expect(fields).toContain('userId String');
|
|
149
|
+
expect(fields).toContain('@relation(fields: [userId], references: [id])');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('1-n: source should get an array field', () => {
|
|
153
|
+
const fields = resolveSourceFields('user', 'post', '1-n');
|
|
154
|
+
expect(fields).toContain('posts Post[]');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('1-1: source should get a unique FK field', () => {
|
|
158
|
+
const fields = resolveSourceFields('profile', 'user', '1-1');
|
|
159
|
+
expect(fields).toContain('userId String? @unique');
|
|
160
|
+
expect(fields).toContain('@relation(fields: [userId], references: [id])');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('n-n: source should get an array field (pivot)', () => {
|
|
164
|
+
const fields = resolveSourceFields('post', 'tag', 'n-n');
|
|
165
|
+
expect(fields).toContain('tags Tag[]');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should throw for unknown relation type', () => {
|
|
169
|
+
expect(() => resolveSourceFields('post', 'user', 'bad')).toThrow('Unknown relation type');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ─── Tests : .nestcraftxrc persistence logic ─────────────────────────────────
|
|
174
|
+
|
|
175
|
+
describe('handleRelationGeneration - .nestcraftxrc update logic', () => {
|
|
176
|
+
it('should add a new relation to config.relations', () => {
|
|
177
|
+
const config = makeConfig({ relations: [] });
|
|
178
|
+
const newRelation = { from: 'post', to: 'user', type: 'n-1' };
|
|
179
|
+
|
|
180
|
+
config.relations.push(newRelation);
|
|
181
|
+
|
|
182
|
+
expect(config.relations).toHaveLength(1);
|
|
183
|
+
expect(config.relations[0]).toMatchObject({ from: 'post', to: 'user', type: 'n-1' });
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should not duplicate a relation already in config.relations', () => {
|
|
187
|
+
const config = makeConfig({
|
|
188
|
+
relations: [{ from: 'post', to: 'user', type: 'n-1' }],
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const source = 'post';
|
|
192
|
+
const target = 'user';
|
|
193
|
+
const type = 'n-1';
|
|
194
|
+
|
|
195
|
+
config.relations = config.relations.filter(
|
|
196
|
+
(r) => !(r.from.toLowerCase() === source && r.to.toLowerCase() === target && r.type === type),
|
|
197
|
+
);
|
|
198
|
+
config.relations.push({ from: source, to: target, type });
|
|
199
|
+
|
|
200
|
+
expect(config.relations).toHaveLength(1);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// tests/unit/throttler.spec.js
|
|
2
|
+
//
|
|
3
|
+
// Tests unitaires pour la configuration de @nestjs/throttler (FEAT-08).
|
|
4
|
+
// Valide :
|
|
5
|
+
// - L'installation du package avec le bon package manager
|
|
6
|
+
// - La configuration correcte de app.module.ts (imports, modules, providers)
|
|
7
|
+
// - L'idempotence de la configuration
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const { setupThrottler } = require('../../utils/setups/setupThrottler');
|
|
11
|
+
const { runCommand } = require('../../utils/shell');
|
|
12
|
+
const { updateFile } = require('../../utils/file-system');
|
|
13
|
+
|
|
14
|
+
// Mock shell et file-system
|
|
15
|
+
jest.mock('../../utils/shell', () => ({
|
|
16
|
+
runCommand: jest.fn().mockResolvedValue(undefined),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
jest.mock('../../utils/file-system', () => ({
|
|
20
|
+
updateFile: jest.fn().mockResolvedValue(undefined),
|
|
21
|
+
createFile: jest.fn().mockResolvedValue(undefined),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
describe('setupThrottler', () => {
|
|
25
|
+
const mockAppModulePath = 'src/app.module.ts';
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
jest.clearAllMocks();
|
|
29
|
+
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
jest.restoreAllMocks();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should run installation with selected package manager', async () => {
|
|
37
|
+
const fsReadSpy = jest.spyOn(fs, 'readFileSync').mockReturnValue(`
|
|
38
|
+
import { Module } from '@nestjs/common';
|
|
39
|
+
@Module({
|
|
40
|
+
imports: [],
|
|
41
|
+
})
|
|
42
|
+
export class AppModule {}
|
|
43
|
+
`);
|
|
44
|
+
const fsWriteSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
|
|
45
|
+
|
|
46
|
+
await setupThrottler({ packageManager: 'pnpm' });
|
|
47
|
+
|
|
48
|
+
expect(runCommand).toHaveBeenCalledWith(
|
|
49
|
+
'pnpm add @nestjs/throttler@^6.0.0',
|
|
50
|
+
expect.any(String),
|
|
51
|
+
expect.any(Object)
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
fsReadSpy.mockRestore();
|
|
55
|
+
fsWriteSpy.mockRestore();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should update app.module.ts with imports, module and guard provider', async () => {
|
|
59
|
+
const originalContent = `import { Module } from '@nestjs/common';
|
|
60
|
+
import { AppController } from './app.controller';
|
|
61
|
+
|
|
62
|
+
@Module({
|
|
63
|
+
imports: [],
|
|
64
|
+
controllers: [AppController],
|
|
65
|
+
providers: [],
|
|
66
|
+
})
|
|
67
|
+
export class AppModule {}`;
|
|
68
|
+
|
|
69
|
+
let writtenContent = '';
|
|
70
|
+
const fsReadSpy = jest.spyOn(fs, 'readFileSync').mockReturnValue(originalContent);
|
|
71
|
+
const fsWriteSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation((path, content) => {
|
|
72
|
+
writtenContent = content;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await setupThrottler({ packageManager: 'npm' });
|
|
76
|
+
|
|
77
|
+
// Doit avoir écrit le fichier
|
|
78
|
+
expect(fsWriteSpy).toHaveBeenCalledWith(mockAppModulePath, expect.any(String), 'utf8');
|
|
79
|
+
|
|
80
|
+
// Doit contenir les nouveaux imports
|
|
81
|
+
expect(writtenContent).toContain("import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';");
|
|
82
|
+
expect(writtenContent).toContain("import { APP_GUARD } from '@nestjs/core';");
|
|
83
|
+
|
|
84
|
+
// Doit contenir ThrottlerModule.forRoot
|
|
85
|
+
expect(writtenContent).toContain('ThrottlerModule.forRoot');
|
|
86
|
+
expect(writtenContent).toContain('ttl: 60000');
|
|
87
|
+
expect(writtenContent).toContain('limit: 10');
|
|
88
|
+
|
|
89
|
+
// Doit contenir le provider APP_GUARD
|
|
90
|
+
expect(writtenContent).toContain('provide: APP_GUARD');
|
|
91
|
+
expect(writtenContent).toContain('useClass: ThrottlerGuard');
|
|
92
|
+
|
|
93
|
+
fsReadSpy.mockRestore();
|
|
94
|
+
fsWriteSpy.mockRestore();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should not modify app.module.ts if Throttler is already configured (idempotence)', async () => {
|
|
98
|
+
const alreadyConfiguredContent = `import { Module } from '@nestjs/common';
|
|
99
|
+
import { ThrottlerModule } from '@nestjs/throttler';
|
|
100
|
+
|
|
101
|
+
@Module({
|
|
102
|
+
imports: [ThrottlerModule.forRoot()],
|
|
103
|
+
})
|
|
104
|
+
export class AppModule {}`;
|
|
105
|
+
|
|
106
|
+
const fsReadSpy = jest.spyOn(fs, 'readFileSync').mockReturnValue(alreadyConfiguredContent);
|
|
107
|
+
const fsWriteSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
|
|
108
|
+
|
|
109
|
+
await setupThrottler({ packageManager: 'npm' });
|
|
110
|
+
|
|
111
|
+
// writeFileSync ne doit pas être appelé car Throttler est déjà configuré
|
|
112
|
+
expect(fsWriteSpy).not.toHaveBeenCalled();
|
|
113
|
+
|
|
114
|
+
fsReadSpy.mockRestore();
|
|
115
|
+
fsWriteSpy.mockRestore();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -24,6 +24,12 @@ const { capitalize } = require("./helpers");
|
|
|
24
24
|
* @param {string} entity - Le nom de l'entité en minuscule (ex: "user", "post")
|
|
25
25
|
*/
|
|
26
26
|
async function safeUpdateAppModule(entity) {
|
|
27
|
+
const isDryRun = process.argv.includes("--dry-run");
|
|
28
|
+
if (isDryRun) {
|
|
29
|
+
console.log(`[DRY-RUN] Simulated: safe update app.module.ts with ${capitalize(entity)}Module`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
const filePath = "src/app.module.ts";
|
|
28
34
|
const moduleName = `${capitalize(entity)}Module`;
|
|
29
35
|
const importLine = `import { ${moduleName} } from 'src/${entity}/${entity}.module';`;
|
|
@@ -124,7 +124,7 @@ async function setupCleanArchitecture(inputs) {
|
|
|
124
124
|
});
|
|
125
125
|
|
|
126
126
|
// 3. Repository Implémentation
|
|
127
|
-
await generateRepository(entity.name, dbConfig.orm);
|
|
127
|
+
await generateRepository(entity.name, dbConfig.orm, entity);
|
|
128
128
|
|
|
129
129
|
// 4. Use Cases
|
|
130
130
|
const useCases = ["Create", "GetById", "GetAll", "Update", "Delete"];
|