diff --git a/.claude/commands/report.md b/.claude/commands/report.md new file mode 100644 index 0000000..9599854 --- /dev/null +++ b/.claude/commands/report.md @@ -0,0 +1,13 @@ +--- +description: Generate MR description in English +--- + +Erstelle mir eine Gesamtzusammenfassung der Änderungen auf englisch, damit ich diese in einem MergeRequest als Description verwenden kann. Führe nur die wesentlichen Punkte dabei auf. + +Bitte strukturiere die Description wie folgt: +- **Summary**: Kurze Zusammenfassung (1-2 Sätze) +- **Changes**: Auflistung der wichtigsten Änderungen +- **Technical Details**: Relevante technische Details falls nötig +- **Testing**: Wie wurde getestet / wie kann getestet werden + +Halte dich kurz und prägnant - fokussiere auf das Wesentliche für Code-Reviewer. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e5dacce..f9833bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@lenne.tech/cli", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@lenne.tech/cli", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "3.926.0", diff --git a/package.json b/package.json index 72b8741..1b127d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lenne.tech/cli", - "version": "1.0.0", + "version": "1.0.1", "description": "lenne.Tech CLI: lt", "keywords": [ "lenne.Tech", diff --git a/src/commands/claude/install-commands.ts b/src/commands/claude/install-commands.ts new file mode 100644 index 0000000..22abd54 --- /dev/null +++ b/src/commands/claude/install-commands.ts @@ -0,0 +1,373 @@ +import { GluegunCommand } from 'gluegun'; +import { homedir } from 'os'; +import { join } from 'path'; + +import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; + +/** + * Get command description from .md frontmatter + */ +function getCommandDescription(commandPath: string, filesystem: any): string { + if (!filesystem.exists(commandPath)) { + return 'No description available'; + } + + const content = filesystem.read(commandPath); + const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); + if (!frontmatterMatch) { + return 'No description available'; + } + + const frontmatter = frontmatterMatch[1]; + const descMatch = frontmatter.match(/description:\s*([^\n]+)/); + + return descMatch ? descMatch[1].trim() : 'No description available'; +} + +/** + * Compare semantic versions + * Returns true if sourceVersion >= targetVersion + */ +function isVersionNewer(sourceVersion: string, targetVersion: string): boolean { + const parseSemver = (version: string): number[] => { + return version.split('.').map(n => parseInt(n, 10) || 0); + }; + + const source = parseSemver(sourceVersion); + const target = parseSemver(targetVersion); + + // Compare major, minor, patch + for (let i = 0; i < 3; i++) { + if (source[i] > target[i]) { + return true; + } + if (source[i] < target[i]) { + return false; + } + } + + // Versions are equal + return true; +} + +/** + * Parse frontmatter from markdown file and extract version + */ +function parseVersion(content: string): null | string { + const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); + if (!frontmatterMatch) { + return null; + } + + const frontmatter = frontmatterMatch[1]; + const versionMatch = frontmatter.match(/version:\s*([^\n]+)/); + + return versionMatch ? versionMatch[1].trim() : null; +} + +/** + * Install Claude Commands to ~/.claude/commands/ or .claude/commands/ + */ +const NewCommand: GluegunCommand = { + alias: ['commands', 'ic'], + description: 'Installs Claude Custom Commands to ~/.claude/commands/ or .claude/commands/ for Claude Code integration. Use --global for global installation, --project for project-specific installation.', + hidden: false, + name: 'install-commands', + run: async (toolbox: ExtendedGluegunToolbox) => { + // Retrieve the tools we need + const { + filesystem, + parameters, + print: { error, info, spin, success }, + prompt, + } = toolbox; + + try { + // Get the CLI installation directory + const cliRoot = join(__dirname, '..', '..'); + const commandsTemplateDir = join(cliRoot, 'templates', 'claude-commands'); + + // Check if claude-commands directory exists + if (!filesystem.exists(commandsTemplateDir)) { + error('Claude commands directory not found in CLI installation.'); + info(`Expected location: ${commandsTemplateDir}`); + info('Please reinstall the CLI or report this issue.'); + return; + } + + // Get all available commands (*.md files, excluding README.md) + const allFiles = filesystem.list(commandsTemplateDir) || []; + const availableCommands = allFiles.filter(file => + file.endsWith('.md') && file !== 'README.md' + ); + + if (availableCommands.length === 0) { + error('No commands found in CLI installation.'); + return; + } + + // Determine installation scope + const cwd = filesystem.cwd(); + let scope: 'global' | 'project' = 'global'; + let commandsDir = join(homedir(), '.claude', 'commands'); + const skipInteractive = parameters.options.y || parameters.options.yes || parameters.options['no-interactive']; + + // Check if user specified scope + if (parameters.options.global) { + scope = 'global'; + commandsDir = join(homedir(), '.claude', 'commands'); + } else if (parameters.options.project) { + scope = 'project'; + commandsDir = join(cwd, '.claude', 'commands'); + } else { + // Detect if we're in a project + let packageJsonPath = null; + let searchDir = cwd; + + // Search up to 3 levels for package.json + for (let i = 0; i < 3; i++) { + const testPath = join(searchDir, 'package.json'); + if (filesystem.exists(testPath)) { + packageJsonPath = testPath; + break; + } + const parent = join(searchDir, '..'); + if (parent === searchDir) break; // Reached root + searchDir = parent; + } + + // If in a project, ask where to install + if (packageJsonPath && !skipInteractive) { + info(''); + info(`Detected project at: ${searchDir}`); + const installToProject = await prompt.confirm( + 'Install commands to this project only? (No = install globally for all projects)', + true + ); + + scope = installToProject ? 'project' : 'global'; + + if (scope === 'project') { + commandsDir = join(searchDir, '.claude', 'commands'); + } + } else if (packageJsonPath && skipInteractive) { + // In non-interactive mode with project detected, default to global + scope = 'global'; + } + } + + // Create commands directory if it doesn't exist + if (!filesystem.exists(commandsDir)) { + filesystem.dir(commandsDir); + } + + info(''); + info('Available commands:'); + info(''); + + // Show all available commands with descriptions + availableCommands.forEach(cmd => { + const cmdPath = join(commandsTemplateDir, cmd); + const desc = getCommandDescription(cmdPath, filesystem); + const cmdName = cmd.replace('.md', ''); + info(` • /${cmdName}`); + info(` ${desc}`); + info(''); + }); + + let commandsToInstall: string[] = []; + + // Check if specific commands provided as parameters + if (parameters.first && parameters.first !== 'all') { + // Non-interactive mode: install specific command(s) + const requestedCommands = parameters.array || [parameters.first]; + + // Add .md extension if not present + const requestedWithExt = requestedCommands.map(c => + c.endsWith('.md') ? c : `${c}.md` + ); + + // Validate requested commands + const invalidCommands = requestedWithExt.filter(c => !availableCommands.includes(c)); + if (invalidCommands.length > 0) { + error(`Invalid command(s): ${invalidCommands.map(c => c.replace('.md', '')).join(', ')}`); + info(''); + info('Available commands:'); + availableCommands.forEach(c => { + const cmdName = c.replace('.md', ''); + info(` • ${cmdName}`); + }); + return; + } + + commandsToInstall = requestedWithExt; + } else if (parameters.first === 'all' || skipInteractive) { + // Install all commands without prompting + commandsToInstall = availableCommands; + if (skipInteractive) { + info('Installing all commands (non-interactive mode)...'); + } + } else { + // Interactive mode: ask if user wants all or select individually + const installAll = await prompt.confirm('Install all commands?', true); + + if (installAll) { + commandsToInstall = availableCommands; + } else { + // Ask for each command + info(''); + info('Select which commands to install:'); + info(''); + + for (const cmd of availableCommands) { + const cmdName = cmd.replace('.md', ''); + const shouldInstall = await prompt.confirm(`Install /${cmdName}?`, true); + if (shouldInstall) { + commandsToInstall.push(cmd); + } + } + + if (commandsToInstall.length === 0) { + info('No commands selected. Installation cancelled.'); + return; + } + } + } + + const installSpinner = spin(`Installing ${commandsToInstall.length} command(s) to ${scope === 'global' ? '~/.claude/commands/' : '.claude/commands/'}...`); + + let copiedCount = 0; + let skippedCount = 0; + let updatedCount = 0; + const skippedFiles: Array<{ file: string; reason: string }> = []; + + // Install each command + for (const cmd of commandsToInstall) { + const sourcePath = join(commandsTemplateDir, cmd); + const targetPath = join(commandsDir, cmd); + + const sourceContent = filesystem.read(sourcePath); + + // Check if target file exists + if (filesystem.exists(targetPath)) { + const targetContent = filesystem.read(targetPath); + + // Parse versions from both files + const sourceVersion = parseVersion(sourceContent); + const targetVersion = parseVersion(targetContent); + + // If both have versions, compare them + if (sourceVersion && targetVersion) { + if (isVersionNewer(sourceVersion, targetVersion)) { + // Source is newer or equal, update + filesystem.write(targetPath, sourceContent); + updatedCount++; + copiedCount++; + } else { + // Target is newer, skip + skippedCount++; + const reason = `local version ${targetVersion} is newer than ${sourceVersion}`; + skippedFiles.push({ file: cmd, reason }); + } + } else if (sourceVersion && !targetVersion) { + // Source has version, target doesn't - update + filesystem.write(targetPath, sourceContent); + updatedCount++; + copiedCount++; + } else { + // No version info, always update (backward compatibility) + filesystem.write(targetPath, sourceContent); + updatedCount++; + copiedCount++; + } + } else { + // Target doesn't exist, create new + filesystem.write(targetPath, sourceContent); + copiedCount++; + } + } + + if (copiedCount === 0 && skippedCount === 0) { + installSpinner.fail(); + error('No command files were processed.'); + return; + } + + if (copiedCount === 0 && skippedCount > 0) { + installSpinner.succeed('All selected commands are already up to date!'); + info(''); + info(`Skipped: ${skippedCount} file(s)`); + info(`Location: ${commandsDir}`); + return; + } + + installSpinner.succeed(`Successfully installed ${commandsToInstall.length} command(s)!`); + info(''); + + if (updatedCount > 0 && skippedCount > 0) { + success(`Updated ${updatedCount} file(s), skipped ${skippedCount} file(s)`); + } else if (updatedCount > 0) { + success(`Updated ${updatedCount} file(s)`); + } else { + success(`Created ${copiedCount} file(s)`); + } + + info(''); + info('Installed commands:'); + commandsToInstall.forEach(cmd => { + const cmdPath = join(commandsTemplateDir, cmd); + const desc = getCommandDescription(cmdPath, filesystem); + const cmdName = cmd.replace('.md', ''); + info(` • /${cmdName}`); + info(` ${desc.substring(0, 80)}${desc.length > 80 ? '...' : ''}`); + }); + + info(''); + info('These commands are now available in Claude Code!'); + + // Show first installed command as example + const firstCmd = commandsToInstall[0]?.replace('.md', ''); + info(`Use them by typing the command name with a leading slash, e.g., /${firstCmd || 'command-name'}`); + info(''); + info('Examples:'); + + // Show up to 5 commands as examples (or all if less than 5) + const exampleCount = Math.min(commandsToInstall.length, 5); + for (let i = 0; i < exampleCount; i++) { + const cmd = commandsToInstall[i]; + const cmdPath = join(commandsTemplateDir, cmd); + const desc = getCommandDescription(cmdPath, filesystem); + const cmdName = cmd.replace('.md', ''); + info(` • /${cmdName} - ${desc}`); + } + + info(''); + info(`Location: ${commandsDir}`); + info(`Scope: ${scope === 'global' ? 'Global (all projects)' : 'Project-specific'}`); + + if (skippedCount > 0) { + info(''); + info(`Note: ${skippedCount} file(s) were skipped because your local versions are newer:`); + skippedFiles.forEach(({ file, reason }) => { + info(` • ${file} (${reason})`); + }); + info(''); + info('To force update, manually delete the files and run this command again.'); + } + + } catch (err) { + error(`Failed to install command(s): ${err.message}`); + info(''); + info('Troubleshooting:'); + info(' • Ensure .claude directory exists and is writable'); + info(' • Check file permissions'); + info(' • Try running with sudo if permission issues persist'); + return; + } + + // For tests + return `claude install-commands`; + }, +}; + +export default NewCommand; diff --git a/src/commands/claude/install-skills.ts b/src/commands/claude/install-skills.ts index fc3022b..c29a7ef 100644 --- a/src/commands/claude/install-skills.ts +++ b/src/commands/claude/install-skills.ts @@ -14,6 +14,10 @@ const SKILL_PERMISSIONS: Record = { 'nest-server-generator': [ 'Bash(lt server:*)', ], + 'story-tdd': [ + 'Bash(npm test:*)', + 'Bash(npm run test:*)', + ], }; /** diff --git a/src/commands/server/add-property.ts b/src/commands/server/add-property.ts index 8fda2a3..e2f75aa 100644 --- a/src/commands/server/add-property.ts +++ b/src/commands/server/add-property.ts @@ -164,54 +164,27 @@ const NewCommand: ExtendedGluegunCommand = { continue; } - const type = ['any', 'bigint', 'boolean', 'never', 'null', 'number', 'string', 'symbol', 'undefined', 'unknown', 'void'].includes(propObj.type) ? propObj.type : pascalCase(propObj.type); - const description = `'${pascalCase(propObj.name)} of ${pascalCase(elementToEdit)}'`; - const typeString = () => { - switch (true) { - case type === 'Json': - return 'JSON'; - - case !!propObj.enumRef: - return propObj.enumRef; - - case !!propObj.schema: - return propObj.schema; + // Use utility function to determine TypeScript type for Model + const tsType = server.getModelClassType(propObj); - case propObj.type === 'ObjectId': - return propObj.reference; + // Build @UnifiedField options; types vary and can't go in standardDeclaration + function constructUnifiedFieldOptions(type: 'create' | 'input' | 'model'): string { + // Use utility functions from server helper + const enumConfig = server.getEnumConfig(propObj); - default: - return pascalCase(type); + // Determine field type based on context + let fieldType: string; + if (type === 'model') { + fieldType = server.getModelFieldType(propObj); + } else { + // Input or CreateInput + fieldType = server.getInputFieldType(propObj, { create: type === 'create' }); } - }; - - // Get TypeScript type (lowercase for native types, PascalCase for custom types) - const standardTypes = ['boolean', 'string', 'number', 'Date']; - const tsType = propObj.schema - ? propObj.schema - : propObj.reference - ? propObj.reference - : standardTypes.includes(propObj.type) - ? propObj.type - : pascalCase(propObj.type); - // Build @UnifiedField options; types vary and can't go in standardDeclaration - function constructUnifiedFieldOptions(type: 'create' | 'input' | 'model'): string { - // For enum arrays, we need both enum config and type - const enumConfig = propObj.enumRef - ? propObj.isArray - ? `enum: { enum: ${propObj.enumRef}, options: { each: true } },\n` - : `enum: { enum: ${propObj.enumRef} },\n` - : ''; - - // Type is only needed for arrays (even enum arrays need type for array notation) - // OR when there's no enum config - const needsType = propObj.isArray || !propObj.enumRef; - const typeConfig = needsType - ? `type: () => ${propObj.isArray ? '[' : ''}${typeString()}${propObj.type === 'ObjectId' || propObj.schema ? (type === 'create' ? 'CreateInput' : type === 'input' ? 'Input' : '') : ''}${propObj.isArray ? ']' : ''}` - : ''; + // Use utility function for type config + const typeConfig = server.getTypeConfig(fieldType, propObj.isArray); // Build mongoose configuration for model type let mongooseConfig = ''; @@ -280,8 +253,10 @@ const NewCommand: ExtendedGluegunCommand = { // Patch input const newInputProperty: OptionalKind = structuredClone(standardDeclaration); newInputProperty.decorators.push({ arguments: [constructUnifiedFieldOptions('input')], name: 'UnifiedField' }); - const inputSuffix = propObj.type === 'ObjectId' || propObj.schema ? 'Input' : ''; - newInputProperty.type = `${typeString()}${inputSuffix}${propObj.isArray ? '[]' : ''}`; + + // Use utility function to determine TypeScript type for Input + const inputTsType = server.getInputClassType(propObj, { create: false }); + newInputProperty.type = `${inputTsType}${propObj.isArray ? '[]' : ''}`; let insertedInputProp; if (inputProperties.length > 0) { @@ -301,8 +276,10 @@ const NewCommand: ExtendedGluegunCommand = { newCreateInputProperty.initializer = 'undefined'; // Override requires = undefined } newCreateInputProperty.decorators.push({ arguments: [constructUnifiedFieldOptions('create')], name: 'UnifiedField' }); - const createSuffix = propObj.type === 'ObjectId' || propObj.schema ? 'CreateInput' : ''; - newCreateInputProperty.type = `${typeString()}${createSuffix}${propObj.isArray ? '[]' : ''}`; + + // Use utility function to determine TypeScript type for CreateInput + const createTsType = server.getInputClassType(propObj, { create: true }); + newCreateInputProperty.type = `${createTsType}${propObj.isArray ? '[]' : ''}`; let insertedCreateInputProp; if (createInputProperties.length > 0) { diff --git a/src/extensions/server.ts b/src/extensions/server.ts index d544fea..d307a6e 100644 --- a/src/extensions/server.ts +++ b/src/extensions/server.ts @@ -267,6 +267,124 @@ export class Server { return false; } + /** + * Determine GraphQL Field type for UnifiedField decorator + * Used for Model, Input, and CreateInput + */ + getInputFieldType(item: ServerProps, options?: { create?: boolean }): string { + const { create } = { create: false, ...options }; + const reference = item.reference?.trim() ? this.pascalCase(item.reference.trim()) : ''; + const schema = item.schema?.trim() ? this.pascalCase(item.schema.trim()) : ''; + const enumRef = item.enumRef?.trim() ? this.pascalCase(item.enumRef.trim()) : ''; + + if (schema) { + // SubObject → Schema + Input/CreateInput suffix + return schema + (create ? 'CreateInput' : 'Input'); + } else if (reference) { + // ObjectId/Reference → always String for IDs + return 'String'; + } else if (enumRef) { + // Enum → use enum name + return enumRef; + } else { + // Standard types or custom types + let fieldType = this.inputFieldTypes[this.pascalCase(item.type)] + || this.pascalCase(item.type) + (create ? 'CreateInput' : 'Input'); + fieldType = this.modelFieldTypes[item.type] + ? this.modelFieldTypes[item.type] + : fieldType; + return fieldType; + } + } + + /** + * Determine TypeScript property type for Input/CreateInput classes + */ + getInputClassType(item: ServerProps, options?: { create?: boolean }): string { + const { create } = { create: false, ...options }; + const reference = item.reference?.trim() ? this.pascalCase(item.reference.trim()) : ''; + const schema = item.schema?.trim() ? this.pascalCase(item.schema.trim()) : ''; + const enumRef = item.enumRef?.trim() ? this.pascalCase(item.enumRef.trim()) : ''; + + if (schema) { + // SubObject → Schema + Input/CreateInput suffix + return schema + (create ? 'CreateInput' : 'Input'); + } else if (reference) { + // ObjectId/Reference → string for IDs + return 'string'; + } else if (enumRef) { + // Enum → use enum name + return enumRef; + } else { + // Standard types or custom types + return this.inputClassTypes[this.pascalCase(item.type)] + || (this.standardTypes.includes(item.type) + ? item.type + : this.pascalCase(item.type) + (create ? 'CreateInput' : 'Input')); + } + } + + /** + * Determine GraphQL Field type for UnifiedField decorator in Model + */ + getModelFieldType(item: ServerProps): string { + const reference = item.reference?.trim() ? this.pascalCase(item.reference.trim()) : ''; + const schema = item.schema?.trim() ? this.pascalCase(item.schema.trim()) : ''; + const enumRef = item.enumRef?.trim() ? this.pascalCase(item.enumRef.trim()) : ''; + + if (reference) { + return reference; + } else if (schema) { + return schema; + } else if (enumRef) { + return enumRef; + } else { + return this.modelFieldTypes[this.pascalCase(item.type)] || this.pascalCase(item.type); + } + } + + /** + * Determine TypeScript property type for Model class + */ + getModelClassType(item: ServerProps): string { + const reference = item.reference?.trim() ? this.pascalCase(item.reference.trim()) : ''; + const schema = item.schema?.trim() ? this.pascalCase(item.schema.trim()) : ''; + const enumRef = item.enumRef?.trim() ? this.pascalCase(item.enumRef.trim()) : ''; + + if (reference) { + return reference; + } else if (schema) { + return schema; + } else if (enumRef) { + return enumRef; + } else { + return this.modelClassTypes[this.pascalCase(item.type)] + || (this.standardTypes.includes(item.type) ? item.type : this.pascalCase(item.type)); + } + } + + /** + * Generate enum configuration for UnifiedField decorator + */ + getEnumConfig(item: ServerProps): string { + const enumRef = item.enumRef?.trim() ? this.pascalCase(item.enumRef.trim()) : ''; + if (!enumRef) { + return ''; + } + + return item.isArray + ? `enum: { enum: ${enumRef}, options: { each: true } },\n ` + : `enum: { enum: ${enumRef} },\n `; + } + + /** + * Generate type configuration for UnifiedField decorator + */ + getTypeConfig(fieldType: string, isArray: boolean): string { + // Type is always needed for all properties + return `type: () => ${isArray ? '[' : ''}${fieldType}${isArray ? ']' : ''}`; + } + /** * Create template string for properties in model */ @@ -331,17 +449,12 @@ export class Server { const reference = item.reference?.trim() ? this.pascalCase(item.reference.trim()) : ''; const schema = item.schema?.trim() ? this.pascalCase(item.schema.trim()) : ''; const enumRef = item.enumRef?.trim() ? this.pascalCase(item.enumRef.trim()) : ''; - const modelFieldType = enumRef - ? 'String' - : schema - ? schema - : this.modelFieldTypes[this.pascalCase(item.type)] || this.pascalCase(item.type); + + // Use utility functions to determine types + const modelFieldType = this.getModelFieldType(item); + const modelClassType = this.getModelClassType(item); const isArray = item.isArray; - const modelClassType - = schema - ? schema - : this.modelClassTypes[this.pascalCase(item.type)] - || (this.standardTypes.includes(item.type) ? item.type : this.pascalCase(item.type)); + const type = this.standardTypes.includes(item.type) ? item.type : this.pascalCase(item.type); if (!this.standardTypes.includes(type) && type !== 'ObjectId' && type !== 'Enum' && type !== 'Json') { mappings[propName] = type; @@ -355,17 +468,10 @@ export class Server { if (this.imports[modelClassType]) { imports[modelClassType] = this.imports[modelClassType]; } - // For enum arrays, we need both enum config and type - const enumConfig = enumRef - ? isArray - ? `enum: { enum: ${enumRef}, options: { each: true } },\n ` - : `enum: { enum: ${enumRef} },\n ` - : ''; - - // Type is only needed for arrays (even enum arrays need type for array notation) - // OR when there's no enum config (references, schemas, standard types) - const needsType = reference || schema || isArray || !enumRef; - const typeConfig = needsType ? `type: () => ${isArray ? '[' : ''}${reference ? reference : enumRef || modelFieldType}${isArray ? ']' : ''}` : ''; + + // Use utility functions for enum and type config + const enumConfig = this.getEnumConfig(item); + const typeConfig = this.getTypeConfig(modelFieldType, isArray); // Build mongoose configuration const mongooseConfig = reference @@ -462,27 +568,22 @@ export class Server { // Process configuration const imports = {}; for (const [name, item] of Object.entries(props)) { - let inputFieldType - = this.inputFieldTypes[this.pascalCase(item.type)] - || (item.enumRef - ? this.pascalCase(item.enumRef) - : this.pascalCase(item.type) + (create ? 'CreateInput' : 'Input')); - inputFieldType = this.modelFieldTypes[item.type] - ? this.modelFieldTypes[item.type] - : inputFieldType; - const inputClassType - = this.inputClassTypes[this.pascalCase(item.type)] - || (this.standardTypes.includes(item.type) - ? item.type - : item.enumRef - ? this.pascalCase(item.enumRef) - : this.pascalCase(item.type) + (create ? 'CreateInput' : 'Input')); + // Skip optional properties in CreateInput (they are inherited from Input) + if (create && item.nullable) { + continue; + } + + // Use utility functions to determine types + const inputFieldType = this.getInputFieldType(item, { create }); + const inputClassType = this.getInputClassType(item, { create }); const propertySuffix = this.propertySuffixTypes[this.pascalCase(item.type)] || ''; + // Use override (not declare) for decorator properties when useDefineForClassFieldsActivated is true const overrideString = this.useDefineForClassFieldsActivated() ? 'override ' : ''; const overrideFlag = create ? overrideString : ''; // When override is set, always use = undefined const propertyUndefinedString = overrideFlag ? ' = undefined;' : undefinedString; + if (this.imports[inputFieldType]) { imports[inputFieldType] = this.imports[inputFieldType]; } @@ -490,18 +591,9 @@ export class Server { imports[inputClassType] = this.imports[inputClassType]; } - // For enum arrays, we need both enum config and type - const enumRef = item.enumRef?.trim() ? this.pascalCase(item.enumRef.trim()) : ''; - const enumConfig = enumRef - ? item.isArray - ? `enum: { enum: ${enumRef}, options: { each: true } },\n ` - : `enum: { enum: ${enumRef} },\n ` - : ''; - - // Type is only needed for arrays (even enum arrays need type for array notation) - // OR when there's no enum config - const needsType = item.isArray || !enumRef; - const typeConfig = needsType ? `type: () => ${item.isArray ? '[' : ''}${inputFieldType}${item.isArray ? ']' : ''}` : ''; + // Use utility functions for enum and type config + const enumConfig = this.getEnumConfig(item); + const typeConfig = this.getTypeConfig(inputFieldType, item.isArray); result += ` /** @@ -509,7 +601,7 @@ export class Server { */ @UnifiedField({ description: '${this.pascalCase(name) + propertySuffix + (modelName ? ` of ${this.pascalCase(modelName)}` : '')}', - ${enumConfig}isOptional: ${nullable}, + ${enumConfig}isOptional: ${nullable || item.nullable}, roles: RoleEnum.S_EVERYONE, ${typeConfig} }) diff --git a/src/interfaces/ServerProps.interface.ts b/src/interfaces/ServerProps.interface.ts index bf8092b..a3ffc8e 100644 --- a/src/interfaces/ServerProps.interface.ts +++ b/src/interfaces/ServerProps.interface.ts @@ -3,11 +3,11 @@ */ export interface ServerProps { declare?: boolean; - enumRef: string; + enumRef?: string; isArray: boolean; name: string; nullable: boolean; - reference: string; - schema: string; + reference?: string; + schema?: string; type: string; } diff --git a/src/templates/claude-commands/code-cleanup.md b/src/templates/claude-commands/code-cleanup.md new file mode 100644 index 0000000..e88704c --- /dev/null +++ b/src/templates/claude-commands/code-cleanup.md @@ -0,0 +1,82 @@ +--- +description: Clean up and optimize code quality +--- + +Perform a complete code cleanup: + +## 📦 1. Import Optimization + +For all modified TypeScript files: +- [ ] Sort imports alphabetically +- [ ] Grouping: External → @lenne.tech → Local +- [ ] Remove unused imports +- [ ] Remove duplicate imports + +## 🔤 2. Property Ordering + +For all Model/Input/Object files: +- [ ] Sort properties alphabetically +- [ ] Decorators consistently ordered +- [ ] Same order in Model, CreateInput, UpdateInput + +## 📝 3. Description Management + +Check all descriptions: +- [ ] Format: "ENGLISH (DEUTSCH)" for German terms +- [ ] Format: "ENGLISH" for English terms +- [ ] Consistency: Same description in Model + Inputs +- [ ] Class-level descriptions present (@ObjectType, @InputType) +- [ ] No missing descriptions + +## ♻️ 4. Code Refactoring + +Search for duplicated code: +- [ ] Is code repeated 2+ times? +- [ ] Can it be extracted into private methods? +- [ ] Are similar code paths consolidatable? +- [ ] Helper functions useful? + +## 🧹 5. Debug Code Removal + +Remove development code: +- [ ] All console.log() statements +- [ ] All console.debug/warn/error (except production logging) +- [ ] Commented-out code +- [ ] Review TODO/FIXME comments + +## ✨ 6. Formatting + +Check code formatting: +- [ ] Consistent indentation (2 or 4 spaces) +- [ ] No extra blank lines +- [ ] Add missing blank lines between sections +- [ ] Remove trailing whitespace + +## 🔧 7. Build & Lint + +Run automatic checks: +```bash +# TypeScript Compilation +npm run build + +# Linting +npm run lint + +# Optional: Auto-Fix +npm run lint:fix +``` + +Fix all errors and warnings! + +## ✅ Final Check + +- [ ] All imports optimized +- [ ] All properties sorted +- [ ] All descriptions correct +- [ ] Code refactored (DRY) +- [ ] Debug code removed +- [ ] Build successful +- [ ] Lint successful +- [ ] Tests still passing + +**Only when everything is ✅: Cleanup completed!** diff --git a/src/templates/claude-commands/mr-description-clipboard.md b/src/templates/claude-commands/mr-description-clipboard.md new file mode 100644 index 0000000..d35b059 --- /dev/null +++ b/src/templates/claude-commands/mr-description-clipboard.md @@ -0,0 +1,48 @@ +--- +description: Generate MR description and save to clipboard +--- + +Create a comprehensive summary of the changes in English for a Merge Request description. + +Please structure the description as follows: +- **Summary**: Brief summary (1-2 sentences) +- **Changes**: List of the most important changes +- **Technical Details**: Relevant technical details if necessary +- **Testing**: How was it tested / how can it be tested + +Keep it short and concise - focus on what's essential for code reviewers. + +**IMPORTANT - CLIPBOARD WORKFLOW:** + +1. First, create the MR description in this format: + +```markdown +## Summary +[Your summary here] + +## Changes +- Change 1 +- Change 2 + +## Technical Details +[Details if necessary] + +## Testing +[Testing approach] +``` + +2. After presenting the description, provide this shell command: + +```bash +cat << 'EOF' | pbcopy +[PASTE THE EXACT MR DESCRIPTION HERE] +EOF +echo "✅ MR description copied to clipboard!" +``` + +3. User can run this command to copy the description to clipboard automatically. + +**Platform-specific commands:** +- macOS: `pbcopy` +- Linux: `xclip -selection clipboard` or `xsel --clipboard` +- Windows: `clip` diff --git a/src/templates/claude-commands/mr-description.md b/src/templates/claude-commands/mr-description.md new file mode 100644 index 0000000..0021e12 --- /dev/null +++ b/src/templates/claude-commands/mr-description.md @@ -0,0 +1,33 @@ +--- +description: Generate Merge Request description +--- + +Create a comprehensive summary of the changes in English so I can use it as a description in a Merge Request. Only include the essential points. + +Please structure the description as follows: +- **Summary**: Brief summary (1-2 sentences) +- **Changes**: List of the most important changes +- **Technical Details**: Relevant technical details if necessary +- **Testing**: How was it tested / how can it be tested + +Keep it short and concise - focus on what's essential for code reviewers. + +**IMPORTANT OUTPUT FORMAT:** +Present the final MR description in a clearly marked code block that is easy to copy: + +```markdown +## Summary +[Your summary here] + +## Changes +- Change 1 +- Change 2 + +## Technical Details +[Details if necessary] + +## Testing +[Testing approach] +``` + +Then add: "✂️ **Copy the markdown block above to use it in your Merge Request.**" diff --git a/src/templates/claude-commands/sec-review.md b/src/templates/claude-commands/sec-review.md new file mode 100644 index 0000000..73477cc --- /dev/null +++ b/src/templates/claude-commands/sec-review.md @@ -0,0 +1,62 @@ +--- +description: Perform security review of code changes +--- + +Perform a complete security review: + +## 🔐 1. Controller/Resolver Security + +Check all modified Controller/Resolver files: +- [ ] Were @Restricted decorators removed or weakened? +- [ ] Were @Roles decorators made more permissive? +- [ ] Are there new endpoints without security decorators? +- [ ] Are the roles appropriate (not too open)? + +## 🔐 2. Model Security + +Check all modified Model files: +- [ ] Is securityCheck() method correctly implemented? +- [ ] Admin check: `user?.hasRole(RoleEnum.ADMIN)` +- [ ] Creator check: `equalIds(user, this.createdBy)` +- [ ] Were security checks weakened? +- [ ] Are sensitive properties protected with @Restricted? + +## 🔐 3. Input Validation + +Check all Input/DTO files: +- [ ] Are all inputs validated? +- [ ] Required fields correctly marked? +- [ ] Type safety ensured? +- [ ] No unsafe data types (e.g., any)? + +## 🔐 4. Ownership & Authorization + +Check service methods: +- [ ] Update/Delete: Ownership checks present? +- [ ] Check: `userId === object.createdBy` OR `user.isAdmin` +- [ ] serviceOptions.roles correctly set? +- [ ] No authorization bypasses? + +## 🔐 5. Data Exposure + +Check GraphQL/REST responses: +- [ ] Sensitive fields marked with `hideField: true`? +- [ ] Passwords/Tokens not in responses? +- [ ] securityCheck() filters correctly? + +## 🔐 6. Test Coverage + +Check tests: +- [ ] Security failure tests present (403 responses)? +- [ ] Tests with different roles (Admin, User, Other)? +- [ ] Ownership tests present? + +## 📋 Report + +Create a list of all findings: +- **Critical**: Severe security issues +- **Warning**: Potential problems +- **Info**: Improvement suggestions +- **OK**: Everything secure + +**On Critical/Warning findings: STOP and inform the developer!** diff --git a/src/templates/claude-commands/skill-optimize.md b/src/templates/claude-commands/skill-optimize.md new file mode 100644 index 0000000..226cd1d --- /dev/null +++ b/src/templates/claude-commands/skill-optimize.md @@ -0,0 +1,140 @@ +--- +description: Optimize Claude skill files if too large +--- + +Analyze and optimize large skill files for better Claude Code performance: + +## 📊 1. Skill File Analysis + +Analyze all skill files: +```bash +# Count lines of all SKILL.md files +find src/templates/claude-skills -name "SKILL.md" -exec wc -l {} \; + +# Or more detailed +for skill in src/templates/claude-skills/*/SKILL.md; do + lines=$(wc -l < "$skill") + name=$(basename $(dirname "$skill")) + echo "$name: $lines lines" +done +``` + +## 🎯 2. Determine Optimization Needs + +**Target Sizes:** +- ✅ **Optimal:** 500-800 lines (fastest loading) +- ⚠️ **Acceptable:** 800-1,800 lines (borderline) +- ❌ **Too Large:** > 1,800 lines (MUST be optimized) + +Identify files over 1,800 lines for optimization. + +## 📑 3. Identify Large Sections + +For each oversized SKILL.md: +```bash +# Show sections with line numbers +grep -n "^## " SKILL.md + +# Calculate section sizes +# Sections > 200 lines are candidates for extraction +``` + +**Typical large sections:** +- Quality Review processes (often 500-1000 lines) +- Security Rules (often 300-500 lines) +- Configuration Guides (often 200-300 lines) +- Test Guidelines (often 400-600 lines) +- Example Collections (often 300-500 lines) + +## 🔧 4. Perform Modularization + +For each large section (> 200 lines): + +**A. Extract section:** +```bash +# Create separate .md file +# e.g., security-rules.md, quality-review.md, configuration.md +``` + +**B. Add frontmatter:** +```markdown +--- +name: skill-name-section-name +version: 1.0.0 +description: What this file contains +--- +``` + +**C. Replace in SKILL.md:** +```markdown +## Section Name + +**📖 For complete [section topic] with all details, see: `section-file.md`** + +**Quick overview:** +- Key point 1 +- Key point 2 +- Key point 3 + +**Critical reminders:** +- [ ] Important checkpoint 1 +- [ ] Important checkpoint 2 +``` + +## 📏 5. Extraction Strategy + +**What to extract:** +- ✅ Detailed process descriptions +- ✅ Extensive examples +- ✅ Long checklists +- ✅ Reference documentation +- ✅ Troubleshooting guides + +**What NOT to extract:** +- ❌ Core workflow (Phases 1-7) +- ❌ Critical warnings (Security, declare keyword) +- ❌ Command syntax (brief reference) +- ❌ Skill description and "When to Use" + +## ✅ 6. Quality Assurance + +After each extraction: +- [ ] SKILL.md has clear reference (📖) to detail file +- [ ] Detail file has frontmatter +- [ ] Compact summary remains in SKILL.md +- [ ] No information is lost +- [ ] Line count significantly reduced + +## 📊 7. Measure Success + +```bash +# Before/After comparison +echo "Original: 3,309 lines" +echo "After optimization: $(wc -l < SKILL.md) lines" +echo "Reduction: $((3309 - $(wc -l < SKILL.md))) lines" +echo "Percentage: $(echo "scale=1; (3309 - $(wc -l < SKILL.md)) * 100 / 3309" | bc)%" +``` + +**Success if:** +- ✅ SKILL.md under 1,800 lines (ideal: under 1,500) +- ✅ 5-8 modular detail files created +- ✅ 30-50% reduction achieved +- ✅ All information still available + +## 🎯 8. Create Report + +Create summary: +``` +=== Skill Optimization Report === + +📁 Optimized Skills: +- skill-name: 3,309 → 1,890 lines (-43%) + +📄 Created Detail Files: +- security-rules.md (9.2K) +- quality-review.md (29K) +- configuration.md (7.0K) +... + +✅ All skills now within optimal range! +``` diff --git a/src/templates/claude-commands/test-generate.md b/src/templates/claude-commands/test-generate.md new file mode 100644 index 0000000..0f53350 --- /dev/null +++ b/src/templates/claude-commands/test-generate.md @@ -0,0 +1,45 @@ +--- +description: Generate comprehensive tests for changes +--- + +Analyze recent changes and create appropriate tests: + +1. **Identify all changed/new modules**: + ```bash + git status --short + git diff --name-only + ``` + +2. **For each new module** in `src/server/modules/`: + - Create E2E test in `tests/modules/.e2e-spec.ts` + - Analyze existing tests as templates + - Fully understand TestHelper (read source code) + +3. **For each modified module**: + - Update existing test in `tests/modules/` + - Test new/changed properties + - Test changed validations + +4. **Security Testing**: + - Check @Restricted/@Roles decorators + - Test with Admin User (user.roles contains 'admin') + - Test with Creator (user.id === object.createdBy) + - Test with Other User (should fail with 403) + - Test permission failures + +5. **Test Execution**: + ```bash + npm run test:e2e + ``` + - On errors: Debug with console.log + - Fix errors + - Re-run tests + +6. **Cleanup**: + - Remove all console.log statements + - Verify tests still pass + +**Important:** +- NEVER weaken @Restricted/@Roles to fix tests +- ALWAYS test with least privileged user +- ALWAYS follow existing test patterns diff --git a/src/templates/claude-skills/nest-server-generator/SKILL.md b/src/templates/claude-skills/nest-server-generator/SKILL.md index b044f85..e792163 100644 --- a/src/templates/claude-skills/nest-server-generator/SKILL.md +++ b/src/templates/claude-skills/nest-server-generator/SKILL.md @@ -1,6 +1,6 @@ --- name: nest-server-generator -version: 1.0.0 +version: 1.0.3 description: PRIMARY expert for ALL NestJS and @lenne.tech/nest-server tasks. ALWAYS use this skill when working in projects with @lenne.tech/nest-server in package.json dependencies (supports monorepos with projects/*, packages/*, apps/* structure), or when asked about NestJS modules, services, controllers, resolvers, models, objects, tests, server creation, debugging, or any NestJS/nest-server development task. Handles lt server commands, security analysis, test creation, and all backend development. ALWAYS reads CrudService base class before working with Services. --- @@ -55,39 +55,68 @@ You are the **PRIMARY expert** for NestJS backend development and the @lenne.tec ## 🚨 CRITICAL SECURITY RULES - READ FIRST -Before you start ANY work with this skill, understand these NON-NEGOTIABLE rules: +**Before you start ANY work, understand these NON-NEGOTIABLE rules:** ### ⛔ NEVER Do This: -1. **NEVER remove or weaken `@Restricted()` decorators** to make tests pass -2. **NEVER change `@Roles()` decorators** to more permissive roles for test convenience -3. **NEVER modify `securityCheck()` logic** to bypass security in tests -4. **NEVER remove class-level `@Restricted(RoleEnum.ADMIN)`** - it's a security fallback +1. **NEVER remove or weaken `@Restricted()` decorators** +2. **NEVER change `@Roles()` decorators** to more permissive roles +3. **NEVER modify `securityCheck()` logic** to bypass security +4. **NEVER remove class-level `@Restricted(RoleEnum.ADMIN)`** ### ✅ ALWAYS Do This: -1. **ALWAYS analyze permissions BEFORE writing tests** (Controller, Model, Service layers) +1. **ALWAYS analyze permissions BEFORE writing tests** 2. **ALWAYS test with the LEAST privileged user** who is authorized -3. **ALWAYS create appropriate test users** for each permission level -4. **ALWAYS adapt tests to security requirements**, never the other way around -5. **ALWAYS ask developer for approval** before changing ANY security decorator -6. **ALWAYS aim for maximum test coverage** (80-100% depending on criticality) +3. **ALWAYS adapt tests to security requirements**, never vice versa +4. **ALWAYS ask developer for approval** before changing ANY security decorator + +**📖 For complete security rules, testing guidelines, and examples, see: `security-rules.md`** + +## 🚨 CRITICAL: NEVER USE `declare` KEYWORD FOR PROPERTIES + +**⚠️ DO NOT use the `declare` keyword when defining properties in classes!** + +### Quick Rule -### 🔑 Permission Hierarchy (Specific Overrides General): ```typescript -@Restricted(RoleEnum.ADMIN) // ← FALLBACK: DO NOT REMOVE -export class ProductController { - @Roles(RoleEnum.S_USER) // ← SPECIFIC: This method is more open - async createProduct() { } // ← S_USER can access (specific wins) +// ❌ WRONG +export class ProductCreateInput extends ProductInput { + declare name: string; // Decorator won't work! +} - async secretMethod() { } // ← ADMIN only (fallback applies) +// ✅ CORRECT +export class ProductCreateInput extends ProductInput { + @UnifiedField({ description: 'Product name' }) + name: string; // Decorator works properly } ``` -**Why class-level `@Restricted(ADMIN)` MUST stay:** -- If someone forgets `@Roles()` on a new method → it's secure by default -- Shows the class is security-sensitive -- Fail-safe protection +**Why**: `declare` prevents decorators from being applied, breaking the decorator system. + +**📖 For detailed explanation and correct patterns, see: `declare-keyword-warning.md`** + +## 🚨 CRITICAL: DESCRIPTION MANAGEMENT + +**⚠️ COMMON MISTAKE:** Descriptions are often applied inconsistently. You MUST follow this process for EVERY component. -**See "CRITICAL: Security & Test Coverage Rules" section for complete details.** +### 3-Step Process + +**1. Extract descriptions** from user's `// comments` + +**2. Format correctly:** +- English input → `'Product name'` +- German input → `'Product name (Produktname)'` +- **⚠️ Fix typos ONLY, NEVER change wording!** + +**3. Apply EVERYWHERE:** +- Model file +- Create Input file +- Update Input file +- Object files (if SubObject) +- Class-level @ObjectType() decorators + +**📖 For detailed formatting rules, examples, and verification checklist, see: `description-management.md`** + +--- ## Core Responsibilities @@ -190,38 +219,22 @@ export class ProductService extends CrudService { ## Configuration File (lt.config.json) -The lenne.tech CLI supports project-level configuration via `lt.config.json` files. This allows you to set default values for commands, eliminating the need for repeated CLI parameters or interactive prompts. +The lenne.tech CLI supports project-level configuration via `lt.config.json` files to set default values for commands. -### File Location and Hierarchy +**📖 For complete configuration guide including structure, options, and examples, see: `configuration.md`** -- **Location**: Place `lt.config.json` in your project root or any parent directory -- **Hierarchy**: The CLI searches from the current directory up to the root, merging configurations -- **Priority** (lowest to highest): - 1. Default values (hardcoded in CLI) - 2. Config from parent directories (higher up = lower priority) - 3. Config from current directory - 4. CLI parameters (`--flag value`) - 5. Interactive user input - -### Configuration Structure +**Quick reference:** +- **Location**: Project root or parent directories +- **Priority**: CLI parameters > Interactive input > Config file > Defaults +- **Key option**: `commands.server.module.controller` - Sets default controller type ("Rest" | "GraphQL" | "Both" | "auto") +**Example config:** ```json { - "meta": { - "version": "1.0.0", - "name": "My Project", - "description": "Optional project description" - }, "commands": { "server": { "module": { - "controller": "Both", - "skipLint": false - }, - "object": { - "skipLint": false - }, - "addProp": { + "controller": "Rest", "skipLint": false } } @@ -229,238 +242,8 @@ The lenne.tech CLI supports project-level configuration via `lt.config.json` fil } ``` -### Available Configuration Options - -**Server Module Configuration (`commands.server.module`)**: -- `controller`: Default controller type (`"Rest"` | `"GraphQL"` | `"Both"` | `"auto"`) -- `skipLint`: Skip lint prompt after module creation (boolean) - -**Server Object Configuration (`commands.server.object`)**: -- `skipLint`: Skip lint prompt after object creation (boolean) - -**Server AddProp Configuration (`commands.server.addProp`)**: -- `skipLint`: Skip lint prompt after adding property (boolean) - -### Using Configuration in Commands - -**Example 1: Configure controller type globally** -```json -{ - "commands": { - "server": { - "module": { - "controller": "Rest" - } - } - } -} -``` - -Now all `lt server module` commands will default to REST controllers: -```bash -# Uses "Rest" from config (no prompt) -lt server module --name Product --prop-name-0 name --prop-type-0 string -``` - -**Example 2: Override config with CLI parameter** -```bash -# Ignores config, uses GraphQL -lt server module --name Product --controller GraphQL -``` - -**Example 3: Auto-detect from config** -```json -{ - "commands": { - "server": { - "module": { - "controller": "auto" - } - } - } -} -``` - -Now the CLI will auto-detect controller type from existing modules without prompting. - -### Managing Configuration - -**Initialize configuration**: -```bash -lt config init -``` - -**Show current configuration** (merged from all hierarchy levels): -```bash -lt config show -``` - -**Get help**: -```bash -lt config help -``` - -### When to Use Configuration - -**✅ Use configuration when:** -- Creating multiple modules with the same controller type -- Working in a team with agreed-upon conventions -- Automating module generation in CI/CD -- You want to skip repetitive prompts - -**❌ Don't use configuration when:** -- Creating a single module with specific requirements -- Each module needs a different controller type -- You're just testing or experimenting - -### Best Practices - -1. **Project Root**: Place `lt.config.json` in your project root -2. **Version Control**: Commit the config file to share with your team -3. **Documentation**: Add a README note explaining the config choices -4. **Override When Needed**: Use CLI parameters to override for special cases - -### 🎯 IMPORTANT: Configuration After Server Creation - -**CRITICAL WORKFLOW**: After creating a new server with `lt server create`, you **MUST** initialize the configuration file to set project conventions. - -#### Automatic Post-Creation Setup - -When you create a new NestJS server, immediately follow these steps: - -1. **Navigate to the API directory**: - ```bash - cd projects/api - ``` - -2. **Create the configuration file manually**: - ```bash - # Create lt.config.json with controller preference - ``` - -3. **Ask the developer for their preference** (if not already specified): - ``` - What controller type do you prefer for new modules in this project? - 1. Rest - REST controllers only - 2. GraphQL - GraphQL resolvers only - 3. Both - Both REST and GraphQL - 4. auto - Auto-detect from existing modules - ``` - -4. **Write the configuration** based on the answer: - ```json - { - "meta": { - "version": "1.0.0" - }, - "commands": { - "server": { - "module": { - "controller": "Rest" - } - } - } - } - ``` - -#### Why This Is Important - -- ✅ **Consistency**: All modules will follow the same pattern -- ✅ **No Prompts**: Developers won't be asked for controller type repeatedly -- ✅ **Team Alignment**: Everyone uses the same conventions -- ✅ **Automation**: Scripts and CI/CD can create modules without interaction - -#### Example Workflow - -```bash -# User creates new server -lt server create --name MyAPI - -# You (Claude) navigate to API directory -cd projects/api - -# You ask the user -"I've created the server. What controller type would you like to use for modules?" -"1. Rest (REST only)" -"2. GraphQL (GraphQL only)" -"3. Both (REST + GraphQL)" -"4. auto (Auto-detect)" - -# User answers: "Rest" - -# You create lt.config.json -{ - "meta": { - "version": "1.0.0" - }, - "commands": { - "server": { - "module": { - "controller": "Rest" - } - } - } -} - -# Confirm to user -"✅ Configuration saved! All new modules will default to REST controllers." -"You can change this anytime by editing lt.config.json or running 'lt config init'." -``` - -#### Configuration Options Explained - -**"Rest"**: -- ✅ Creates REST controllers (`@Controller()`) -- ❌ No GraphQL resolvers -- ❌ No PubSub integration -- **Best for**: Traditional REST APIs, microservices - -**"GraphQL"**: -- ❌ No REST controllers -- ✅ Creates GraphQL resolvers (`@Resolver()`) -- ✅ Includes PubSub for subscriptions -- **Best for**: GraphQL-first APIs, real-time apps - -**"Both"**: -- ✅ Creates REST controllers -- ✅ Creates GraphQL resolvers -- ✅ Includes PubSub -- **Best for**: Hybrid APIs, gradual migration - -**"auto"**: -- 🤖 Analyzes existing modules -- 🤖 Detects pattern automatically -- 🤖 No user prompt -- **Best for**: Following existing conventions - -#### When NOT to Create Config - -Skip config creation if: -- ❌ User is just testing/experimenting -- ❌ User explicitly says "no configuration" -- ❌ Project already has lt.config.json - -### Integration with Commands - -When generating code, **ALWAYS check for configuration**: -1. Load config via `lt config show` or check for `lt.config.json` -2. Use configured values in command construction -3. Only pass CLI parameters when overriding config - -**Example: Generating module with config** -```bash -# Check if config exists and what controller type is configured -# If config has "controller": "Rest", use it -lt server module --name Product --prop-name-0 name --prop-type-0 string - -# If config has "controller": "auto", let CLI detect -lt server module --name Order --prop-name-0 total --prop-type-0 number - -# Override config when needed -lt server module --name User --controller Both -``` - -## Command Syntax Reference +**Initialize config**: `lt config init` +**Show current config**: `lt config show` ### lt server module Creates a complete NestJS module with model, service, controller/resolver, and DTOs. @@ -876,29 +659,177 @@ export class BuyerProfile extends Profile { ... } ### Phase 5: Description Management -**Rule**: All descriptions follow format: `"ENGLISH_DESCRIPTION (DEUTSCHE_BESCHREIBUNG)"` +**⚠️ CRITICAL PHASE - Refer to "CRITICAL: DESCRIPTION MANAGEMENT" section at the top of this document!** + +This phase is often done incorrectly. Follow these steps EXACTLY: + +#### Step 5.1: Extract Descriptions from User Input + +**BEFORE applying any descriptions, review the original specification:** + +Go back to the user's original specification and extract ALL comments that appear after `//`: + +``` +Module: Product +- name: string // Product name +- price: number // Produktpreis +- description?: string // Produktbeschreibung +- stock: number // Current inventory + +SubObject: Address +- street: string // Straße +- city: string // City name +- zipCode: string // Postleitzahl +``` + +**Create a mapping**: +``` +Product.name → "Product name" (English) +Product.price → "Produktpreis" (German) +Product.description → "Produktbeschreibung" (German) +Product.stock → "Current inventory" (English) +Address.street → "Straße" (German) +Address.city → "City name" (English) +Address.zipCode → "Postleitzahl" (German) +``` + +#### Step 5.2: Format Descriptions + +**Rule**: `"ENGLISH_DESCRIPTION (DEUTSCHE_BESCHREIBUNG)"` -**Process for each property**: +Apply formatting rules: 1. **If comment is in English**: ``` - // Street name + // Product name ``` - → Use as: `description: 'Street name'` + → Use as: `description: 'Product name'` + + Fix typos if needed: + ``` + // Prodcut name (typo) + ``` + → Use as: `description: 'Product name'` (typo corrected) 2. **If comment is in German**: + ``` + // Produktpreis + ``` + → Translate and add original: `description: 'Product price (Produktpreis)'` + ``` // Straße ``` → Translate and add original: `description: 'Street (Straße)'` -3. **If no comment**: - → Create meaningful description: `description: 'User email address'` + Fix typos in original: + ``` + // Postleizahl (typo: missing 't') + ``` + → Translate and add corrected: `description: 'Postal code (Postleitzahl)'` + +3. **If no comment provided**: + → Create meaningful English description: `description: 'User email address'` + +**⚠️ CRITICAL - Preserve Original Wording**: + +- ✅ **DO:** Fix spelling/typos only +- ❌ **DON'T:** Rephrase, expand, or improve wording +- ❌ **DON'T:** Change terms (they may be predefined/referenced by external systems) + +**Examples**: +``` +✅ CORRECT: +// Straße → 'Street (Straße)' (preserve word) +// Produkt → 'Product (Produkt)' (don't add "name") +// Status → 'Status (Status)' (same in both languages) + +❌ WRONG: +// Straße → 'Street name (Straßenname)' (changed word!) +// Produkt → 'Product name (Produktname)' (added word!) +// Status → 'Current status (Aktueller Status)' (added word!) +``` + +#### Step 5.3: Apply Descriptions EVERYWHERE + +**🚨 MOST IMPORTANT: Apply SAME description to ALL files!** + +For **EVERY property in EVERY Module**: + +1. Open `.model.ts` → Add description to property +2. Open `inputs/-create.input.ts` → Add SAME description to property +3. Open `inputs/.input.ts` → Add SAME description to property + +For **EVERY property in EVERY SubObject**: + +1. Open `objects//.object.ts` → Add description to property +2. Open `objects//-create.input.ts` → Add SAME description to property +3. Open `objects//.input.ts` → Add SAME description to property + +**Example for Module "Product" with property "price"**: + +```typescript +// File: src/server/modules/product/product.model.ts +@UnifiedField({ description: 'Product price (Produktpreis)' }) +price: number; + +// File: src/server/modules/product/inputs/product-create.input.ts +@UnifiedField({ description: 'Product price (Produktpreis)' }) +price: number; + +// File: src/server/modules/product/inputs/product.input.ts +@UnifiedField({ description: 'Product price (Produktpreis)' }) +price?: number; +``` + +**Example for SubObject "Address" with property "street"**: + +```typescript +// File: src/server/common/objects/address/address.object.ts +@UnifiedField({ description: 'Street (Straße)' }) +street: string; + +// File: src/server/common/objects/address/address-create.input.ts +@UnifiedField({ description: 'Street (Straße)' }) +street: string; + +// File: src/server/common/objects/address/address.input.ts +@UnifiedField({ description: 'Street (Straße)' }) +street?: string; +``` + +#### Step 5.4: Add Class-Level Descriptions + +Also add descriptions to the `@ObjectType()` and `@InputType()` decorators: + +```typescript +@ObjectType({ description: 'Product entity (Produkt-Entität)' }) +export class Product extends CoreModel { ... } + +@InputType({ description: 'Product creation data (Produkt-Erstellungsdaten)' }) +export class ProductCreateInput { ... } + +@InputType({ description: 'Product update data (Produkt-Aktualisierungsdaten)' }) +export class ProductInput { ... } +``` + +#### Step 5.5: Verify Consistency + +After applying all descriptions, verify: + +- [ ] All user-provided comments extracted and processed +- [ ] All German descriptions translated to format: `ENGLISH (DEUTSCH)` +- [ ] All English descriptions kept as-is +- [ ] Module Model has descriptions on all properties +- [ ] Module CreateInput has SAME descriptions on all properties +- [ ] Module UpdateInput has SAME descriptions on all properties +- [ ] SubObject has descriptions on all properties +- [ ] SubObject CreateInput has SAME descriptions on all properties +- [ ] SubObject UpdateInput has SAME descriptions on all properties +- [ ] Class-level decorators have descriptions +- [ ] NO inconsistencies (same property, different descriptions) -4. **Apply same description to**: - - Model property - - Input property (both create and update) - - Output property +**If ANY checkbox is unchecked, STOP and fix before continuing to Phase 6!** ### Phase 6: Enum File Creation @@ -920,6 +851,99 @@ export enum StatusEnum { ### Phase 7: API Test Creation +**⚠️ CRITICAL: Test Type Requirement** + +**ONLY create API tests using TestHelper - NEVER create direct Service tests!** + +- ✅ **DO:** Create tests that call REST endpoints or GraphQL queries/mutations using `TestHelper` +- ✅ **DO:** Test through the API layer (Controller/Resolver → Service → Database) +- ❌ **DON'T:** Create tests that directly instantiate or call Service methods +- ❌ **DON'T:** Create unit tests for Services (e.g., `user.service.spec.ts`) +- ❌ **DON'T:** Mock dependencies or bypass the API layer + +**Why API tests only?** +- API tests validate the complete security model (decorators, guards, permissions) +- Direct Service tests bypass authentication and authorization checks +- TestHelper provides all necessary tools for comprehensive API testing + +**Exception: Direct database/service access for test setup/cleanup ONLY** + +Direct database or service access is ONLY allowed for: + +- ✅ **Test Setup (beforeAll/beforeEach)**: + - Setting user roles in database: `await db.collection('users').updateOne({ _id: userId }, { $set: { roles: ['admin'] } })` + - Setting verified flag: `await db.collection('users').updateOne({ _id: userId }, { $set: { verified: true } })` + - Creating prerequisite test data that can't be created via API + +- ✅ **Test Cleanup (afterAll/afterEach)**: + - Deleting test objects: `await db.collection('products').deleteMany({ createdBy: testUserId })` + - Cleaning up test data: `await db.collection('users').deleteOne({ email: 'test@example.com' })` + +- ❌ **NEVER for testing functionality**: + - Don't call `userService.create()` to test user creation - use API endpoint! + - Don't call `productService.update()` to test updates - use API endpoint! + - Don't access database to verify results - query via API instead! + +**Example of correct usage:** + +```typescript +describe('Product Tests', () => { + let adminToken: string; + let userId: string; + + beforeAll(async () => { + // ✅ ALLOWED: Direct DB access for setup + const user = await testHelper.rest('/auth/signup', { + method: 'POST', + payload: { email: 'admin@test.com', password: 'password' } + }); + userId = user.id; + + // ✅ ALLOWED: Direct DB manipulation for test setup + await db.collection('users').updateOne( + { _id: new ObjectId(userId) }, + { $set: { roles: ['admin'], verified: true } } + ); + + // Get token via API + const auth = await testHelper.rest('/auth/signin', { + method: 'POST', + payload: { email: 'admin@test.com', password: 'password' } + }); + adminToken = auth.token; + }); + + it('should create product', async () => { + // ✅ CORRECT: Test via API + const result = await testHelper.rest('/api/products', { + method: 'POST', + payload: { name: 'Test Product' }, + token: adminToken + }); + + expect(result.name).toBe('Test Product'); + + // ❌ WRONG: Don't verify via DB + // const dbProduct = await db.collection('products').findOne({ _id: result.id }); + + // ✅ CORRECT: Verify via API + const fetched = await testHelper.rest(`/api/products/${result.id}`, { + method: 'GET', + token: adminToken + }); + expect(fetched.name).toBe('Test Product'); + }); + + afterAll(async () => { + // ✅ ALLOWED: Direct DB access for cleanup + await db.collection('products').deleteMany({ createdBy: userId }); + await db.collection('users').deleteOne({ _id: new ObjectId(userId) }); + }); +}); +``` + +--- + **⚠️ CRITICAL: Test Creation Process** Creating API tests is NOT just about testing functionality - it's about **validating the security model**. You MUST follow this exact process: @@ -1594,7 +1618,20 @@ After generation, verify: - [ ] All Objects created - [ ] All Modules created - [ ] All properties in alphabetical order -- [ ] All descriptions follow format: "ENGLISH (DEUTSCH)" +- [ ] **DESCRIPTIONS (Critical - check thoroughly):** + - [ ] All user-provided comments (after `//`) extracted from specification + - [ ] All German descriptions translated to format: `ENGLISH (DEUTSCH)` + - [ ] All English descriptions kept as-is (spelling corrected) + - [ ] ALL Module Models have descriptions on all properties + - [ ] ALL Module CreateInputs have SAME descriptions + - [ ] ALL Module UpdateInputs have SAME descriptions + - [ ] ALL SubObjects have descriptions on all properties + - [ ] ALL SubObject CreateInputs have SAME descriptions + - [ ] ALL SubObject UpdateInputs have SAME descriptions + - [ ] ALL `@ObjectType()` decorators have descriptions + - [ ] ALL `@InputType()` decorators have descriptions + - [ ] NO inconsistencies (same property, different descriptions in different files) + - [ ] NO German-only descriptions (must be translated) - [ ] Inheritance properly implemented - [ ] Required fields correctly set in CreateInputs - [ ] Enum files created in `src/server/common/enums/` @@ -1747,1036 +1784,57 @@ import { User } from '../../user/user.model'; ## ⚠️ CRITICAL: Security & Test Coverage Rules -### Rule 1: NEVER Weaken Security for Test Convenience - -**❌ ABSOLUTELY FORBIDDEN:** -```typescript -// BEFORE (secure): -@Restricted(RoleEnum.ADMIN) -export class ProductController { - @Roles(RoleEnum.S_USER) - async createProduct() { ... } -} - -// AFTER (FORBIDDEN - security weakened!): -// @Restricted(RoleEnum.ADMIN) ← NEVER remove this! -export class ProductController { - @Roles(RoleEnum.S_USER) - async createProduct() { ... } -} -``` - -**🚨 CRITICAL RULE:** -- **NEVER remove or weaken `@Restricted()` decorators** on Controllers, Resolvers, Models, or Objects -- **NEVER change `@Roles()` decorators** to more permissive roles just to make tests pass -- **NEVER modify `securityCheck()` logic** to bypass security for testing - -**If tests fail due to permissions:** -1. ✅ **CORRECT**: Adjust the test to use the appropriate user/token -2. ✅ **CORRECT**: Create test users with the required roles -3. ❌ **WRONG**: Weaken security to make tests pass - -**Any security changes MUST:** -- Be discussed with the developer FIRST -- Have a solid business justification -- Be explicitly approved by the developer -- Be documented with the reason - -### Rule 2: Understanding Permission Hierarchy - -**⭐ Key Concept: Specific Overrides General** - -The `@Restricted()` decorator on a class acts as a **security fallback** - if a method/property doesn't specify permissions, it inherits the class-level restriction. This is a **security-by-default** pattern. - -**Example - Controller/Resolver:** -```typescript -@Restricted(RoleEnum.ADMIN) // ← FALLBACK: Protects everything by default -export class ProductController { - - @Roles(RoleEnum.S_EVERYONE) // ← SPECIFIC: This method is MORE open - async getPublicProducts() { - // Anyone can access this - } - - @Roles(RoleEnum.S_USER) // ← SPECIFIC: This method is MORE open - async getMyProducts() { - // Any signed-in user can access this - } - - async adminOnlyMethod() { // ← NO DECORATOR: Falls back to @Restricted(ADMIN) - // Only admins can access this - } -} -``` - -**Why `@Restricted(RoleEnum.ADMIN)` MUST stay:** -- **Fail-safe**: If someone forgets `@Roles()` on a new method, it's protected by default -- **Security**: Without it, a method without `@Roles()` would be open to everyone -- **Intent**: Shows the class is security-sensitive - -**Example - Model/Object:** -```typescript -@Restricted(RoleEnum.ADMIN) // ← FALLBACK: Protects all properties by default -export class Product extends CoreModel { - - @Restricted(RoleEnum.S_EVERYONE) // ← SPECIFIC: This field is MORE open - name: string; - - @Restricted(RoleEnum.S_USER) // ← SPECIFIC: This field is MORE open - description: string; - - secretInternalNotes: string; // ← NO DECORATOR: Falls back to ADMIN only - - securityCheck(user: User) { - // Controls who can see the entire object - if (user?.hasRole(RoleEnum.ADMIN)) return this; - if (this.isPublic) return this; - return undefined; - } -} -``` - -### Rule 3: Test Coverage Strategy +**This section provides detailed rules for security and testing. It's duplicated at the beginning of this file for visibility.** -**🎯 Goal: Maximum Coverage WITHOUT Compromising Security** - -**Approach:** -1. **Analyze permissions thoroughly** (as described in Phase 7) -2. **Create appropriate test users** for each permission level: - ```typescript - const adminUser = await testHelper.createUser({ roles: [RoleEnum.ADMIN] }); - const regularUser = await testHelper.createUser({ roles: [RoleEnum.S_USER] }); - const creatorUser = await testHelper.createUser({ roles: [RoleEnum.S_USER] }); - ``` -3. **Test with the LEAST privileged user** who should have access -4. **Also test with UNAUTHORIZED users** to verify security -5. **Document why certain operations require certain roles** - -**Coverage Priorities:** -1. **100% coverage** for all security-critical code (securityCheck, guards) -2. **100% coverage** for all endpoints (both success and failure cases) -3. **100% coverage** for all permission combinations -4. **90%+ coverage** for business logic -5. **Complete coverage** of error paths and edge cases - -**Testing Strategy:** -```typescript -describe('ProductController', () => { - describe('createProduct', () => { - // Test with minimum required privilege - it('should create product as S_USER', async () => { - // ✅ Tests with least privileged user who should succeed - }); +**📖 For the complete content with all rules, examples, and testing strategies, see: `security-rules.md`** - // Test security failure - it('should FAIL to create product without auth', async () => { - // ✅ Verifies security works - }); - }); +**Quick reminder - Core rules:** +1. **NEVER weaken @Restricted or @Roles decorators** to make tests pass +2. **NEVER modify securityCheck() logic** to bypass security +3. **ALWAYS test with least privileged user** who is authorized +4. **ALWAYS create appropriate test users** for each permission level +5. **ALWAYS ask developer** before changing ANY security decorator +6. **Aim for 80-100% test coverage** without compromising security - describe('adminOnlyMethod', () => { - it('should execute as admin', async () => { - // ✅ Uses admin token (required for this method) - }); +**Testing approach:** +- Analyze permissions first +- Create test users for each role +- Test happy path with appropriate user +- Test security failures (403 responses) +- Document why certain roles are required - it('should FAIL for regular user', async () => { - // ✅ Verifies fallback to @Restricted(ADMIN) works - }); +## Phase 8: Pre-Report Quality Review - it('should FAIL without auth', async () => { - // ✅ Verifies security - }); - }); -}); -``` - -### Rule 4: When Security Changes Are Necessary - -**If you genuinely believe a security restriction is too strict:** - -1. **STOP** - Do NOT make the change -2. **Analyze** - Why is the restriction failing the test? -3. **Document** - Prepare a clear explanation: - - What restriction exists? - - Why does it block the test? - - What business case requires opening it? - - What security risks does opening it introduce? - - What mitigation strategies exist? -4. **Ask the developer**: - ``` - "I've analyzed the permissions for [Method/Property] and found: - - Current restriction: @Restricted(RoleEnum.ADMIN) - - Test requirement: S_USER needs access to [do X] - - Business justification: [explain why] - - Security impact: [explain risks] - - Mitigation: [how to minimize risk] - - Should I: - A) Keep security as-is and adjust the test - B) Change to @Restricted(RoleEnum.S_USER) with your approval - C) Something else?" - ``` -5. **Wait for explicit approval** before changing ANY security decorator -6. **Document the decision** in code comments: - ```typescript - // Changed from ADMIN to S_USER on 2024-01-15 - // Reason: Users need to create their own products - // Approved by: [Developer Name] - @Restricted(RoleEnum.S_USER) - ``` - -**Remember:** -- Security is NOT negotiable for test convenience -- Tests must adapt to security requirements, not vice versa -- When in doubt, keep it secure and ask - -## Phase 8: Pre-Report Quality Review - -**CRITICAL**: Before creating the final report, you MUST perform a comprehensive quality review: - -### Step 1: Identify All Changes - -Use git to identify all created and modified files: - -```bash -git status --short -git diff --name-only -``` - -For each file, review: -- All newly created files -- All modified files -- File structure and organization - -### Step 2: Test Management - -**CRITICAL**: Ensure tests are created/updated for all changes: - -#### Step 2.1: Analyze Existing Tests FIRST - -**BEFORE creating or modifying ANY tests, you MUST thoroughly analyze existing tests**: - -1. **Identify all existing test files**: - ```bash - # List all test directories and files - ls -la tests/ - ls -la tests/modules/ - find tests -name "*.e2e-spec.ts" -type f - ``` - -2. **Read multiple existing test files completely**: - ```bash - # Read at least 2-3 different module tests to understand patterns - cat tests/modules/user.e2e-spec.ts - cat tests/modules/.e2e-spec.ts - - # Also check the common and project test files - cat tests/common.e2e-spec.ts - cat tests/project.e2e-spec.ts - ``` - -3. **CRITICAL: Understand the TestHelper thoroughly**: - - **Before creating any tests, you MUST understand the TestHelper from @lenne.tech/nest-server**: - - ```bash - # Read the TestHelper source code to understand its capabilities - cat node_modules/@lenne.tech/nest-server/src/test/test.helper.ts - ``` - - **Analyze the TestHelper to understand**: - - **Available methods**: What methods does TestHelper provide? - - **Configuration options**: How can TestHelper be configured? - - **GraphQL support**: How to use `graphQl()` method? What parameters does it accept? - - **REST support**: How to use `rest()` method? What parameters does it accept? - - **Authentication**: How does TestHelper handle tokens and authentication? - - **Request building**: How are requests constructed? What options are available? - - **Response handling**: How are responses processed? What format is returned? - - **Error handling**: How does TestHelper handle errors and failures? - - **Helper utilities**: What additional utilities are available? - - **Document your findings**: - ```typescript - // Example: Understanding TestHelper.graphQl() - // Method signature: graphQl(options: GraphQLOptions, config?: RequestConfig) - // GraphQLOptions: { name, type (QUERY/MUTATION), arguments, fields } - // RequestConfig: { token, statusCode, headers } - // Returns: Parsed response data or error - - // Example: Understanding TestHelper.rest() - // Method signature: rest(method: HttpMethod, path: string, options?: RestOptions) - // HttpMethod: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' - // RestOptions: { body, token, statusCode, headers } - // Returns: Response data or error - ``` - - **Common TestHelper patterns to understand**: - - How to execute GraphQL queries/mutations with `graphQl()` - - How to execute REST requests with `rest()` - - How to pass authentication tokens (same for both methods) - - How to handle expected errors (statusCode parameter) - - How to work with response data - - How to structure test data - - When to use GraphQL vs REST methods - - **Only after fully understanding TestHelper, proceed to next step.** - -4. **Understand the testing approach used**: - - Which test framework? (Jest, Mocha, etc.) - - Which testing utilities? (@lenne.tech/nest-server testHelper, custom helpers) - - How is the test app initialized? (beforeAll setup) - - How are test users/auth handled? - - How is test data created and cleaned up? - - What assertion library? (expect, should, etc.) - - Are there custom matchers? - -5. **Document the patterns you observe**: - - **Import patterns**: Which modules are imported? In what order? - - **Setup patterns**: How is beforeAll/beforeEach structured? - - **Auth patterns**: How do tests authenticate? Token handling? - - **Test structure**: Describe blocks organization? Test naming conventions? - - **CRUD patterns**: How are create/read/update/delete tested? - - **Assertion patterns**: What assertions are used? How detailed? - - **Cleanup patterns**: How is afterAll/afterEach structured? - - **Error testing**: How are failures/validations tested? - -6. **Verify existing tests run successfully**: - ```bash - # Run existing tests to ensure they pass - npm run test:e2e - - # If any fail, understand why before proceeding - # Your new/modified tests MUST NOT break existing tests - ``` - -7. **Create a mental checklist**: - - [ ] I have read and understand the TestHelper source code - - [ ] I understand TestHelper methods and configuration - - [ ] I understand how to use graphQl() method (GraphQL queries/mutations) - - [ ] I understand how to use rest() method (REST endpoints) - - [ ] I understand when to use graphQl() vs rest() - - [ ] I understand TestHelper authentication and error handling - - [ ] I understand which test helpers/utilities are used - - [ ] I understand the authentication/authorization pattern - - [ ] I understand the test data lifecycle (create/cleanup) - - [ ] I understand the assertion patterns - - [ ] I understand the error testing approach - - [ ] All existing tests pass before I make changes - -**Only after completing this analysis, proceed to create or modify tests.** - -#### Step 2.1.1: Understanding Permissions and User Rights in Tests - -**CRITICAL**: Before creating tests, you MUST understand the 3-layer permission system: - -**Important Definitions**: - -- **Admin User**: A user whose `roles` array contains `'admin'` - ```typescript - // Example admin user - { - id: '123', - email: 'admin@test.com', - roles: ['admin', 'user'] // ← Contains 'admin' - } - ``` - -- **Creator**: The user who created an object, identified by matching IDs - ```typescript - // User who created the object - const user = { id: 'user-123', email: 'creator@test.com' }; - - // Object created by this user - const product = { - id: 'product-456', - name: 'Test Product', - createdBy: 'user-123' // ← Matches user.id → This user is the CREATOR - }; - - // Different user (NOT the creator) - const otherUser = { id: 'user-789', email: 'other@test.com' }; - // otherUser.id !== product.createdBy → NOT the creator! - ``` - -**The Three Permission Layers**: - -1. **Controller/Resolver Layer** (`@Roles()` decorator): - - Controls WHO can call the endpoint - - Example: `@Roles(RoleEnum.ADMIN)` → Only admins can call this endpoint - - Example: `@Roles(RoleEnum.S_USER)` → All signed-in users can call - -2. **Service Layer** (`serviceOptions.roles` parameter): - - Controls what permissions are checked during service processing - - Example: Update/Delete often require `[RoleEnum.ADMIN, RoleEnum.S_CREATOR]` - - The creator can update/delete their own items - -3. **Model Layer** (`securityCheck()` method): - - Controls WHAT data is returned to the user - - Standard implementation: - ```typescript - securityCheck(user: User, force?: boolean) { - // Admins see everything (user.roles contains 'admin') - if (force || user?.hasRole(RoleEnum.ADMIN)) { - return this; - } - // Only creator can see their own data (user.id === this.createdBy) - if (!equalIds(user, this.createdBy)) { - return undefined; // Non-creator gets nothing! - } - return this; - } - ``` - - **Key checks**: - - `user?.hasRole(RoleEnum.ADMIN)` → Returns `true` if `user.roles.includes('admin')` - - `equalIds(user, this.createdBy)` → Returns `true` if `user.id === this.createdBy` - -**Default Permission Behavior**: -- **Create**: Usually accessible to signed-in users (`RoleEnum.S_USER`) -- **Read/List**: Usually accessible to signed-in users, but securityCheck filters results -- **Update**: Only ADMIN or CREATOR (via `serviceOptions.roles` check) -- **Delete**: Only ADMIN or CREATOR (via `serviceOptions.roles` check) - -**Analyzing Permissions Before Creating Tests**: - -Before writing tests, check these 3 locations: - -1. **Check Controller/Resolver decorators**: - ```typescript - // In product.resolver.ts - @Roles(RoleEnum.ADMIN) // ← WHO can call this? - @Query(() => Product) - async getProduct(@Args('id') id: string) { ... } - - @Roles(RoleEnum.S_USER) // ← All signed-in users - @Mutation(() => Product) - async createProduct(@Args('input') input: ProductCreateInput) { ... } - ``` - -2. **Check Model/Object `@Restricted` decorators**: - ```typescript - // In product.model.ts - @Restricted(RoleEnum.ADMIN) // ← Model-level restriction - export class Product extends CoreModel { - - @Restricted(RoleEnum.ADMIN) // ← Property-level restriction - @UnifiedField() - internalNotes?: string; - } - ``` - -3. **Check Model `securityCheck()` logic**: - ```typescript - // In product.model.ts - securityCheck(user: User, force?: boolean) { - // Admin check: user.roles contains 'admin' - if (force || user?.hasRole(RoleEnum.ADMIN)) { - return this; // Admin sees all - } - - // Custom logic: Allow public products for everyone - if (this.isPublic) { - return this; - } - - // Creator check: user.id === this.createdBy - if (!equalIds(user, this.createdBy)) { - return undefined; // Non-creator gets nothing - } - return this; // Creator sees their own product - } - ``` - -**Creating Appropriate Test Users**: - -Based on permission analysis, create appropriate test users: - -```typescript -describe('Product Module', () => { - let testHelper: TestHelper; - let adminToken: string; - let userToken: string; - let otherUserToken: string; - let createdProductId: string; - - beforeAll(async () => { - testHelper = new TestHelper(app); - - // Admin user (user.roles contains 'admin') - const adminAuth = await testHelper.graphQl({ - name: 'signIn', - type: TestGraphQLType.MUTATION, - arguments: { email: 'admin@test.com', password: 'admin' }, - fields: ['token', 'user { id email roles }'] - }); - adminToken = adminAuth.token; - // adminAuth.user.roles = ['admin', 'user'] ← Contains 'admin' - - // Regular user (will be the creator of test objects) - const userAuth = await testHelper.graphQl({ - name: 'signIn', - type: TestGraphQLType.MUTATION, - arguments: { email: 'user@test.com', password: 'user' }, - fields: ['token', 'user { id email roles }'] - }); - userToken = userAuth.token; - // When this user creates an object → object.createdBy = userAuth.user.id - - // Another regular user (will NOT be the creator) - const otherUserAuth = await testHelper.graphQl({ - name: 'signIn', - type: TestGraphQLType.MUTATION, - arguments: { email: 'other@test.com', password: 'other' }, - fields: ['token', 'user { id email roles }'] - }); - otherUserToken = otherUserAuth.token; - // otherUserAuth.user.id !== object.createdBy → NOT the creator - }); -}); -``` - -**Test Structure Based on Permissions**: - -```typescript -describe('Product Module', () => { - // ... setup with adminToken, userToken, otherUserToken - - describe('Create Product', () => { - it('should create product as regular user', async () => { - const result = await testHelper.graphQl({ - name: 'createProduct', - type: TestGraphQLType.MUTATION, - arguments: { input: { name: 'Test Product', price: 99.99 } }, - fields: ['id', 'name', 'createdBy { id }'] - }, { token: userToken }); // ← Created by userToken - - expect(result.name).toBe('Test Product'); - // result.createdBy.id now equals userAuth.user.id - // → userToken is the CREATOR of this product - createdProductId = result.id; - }); - }); - - describe('Update Product', () => { - it('should update product as creator', async () => { - const result = await testHelper.graphQl({ - name: 'updateProduct', - type: TestGraphQLType.MUTATION, - arguments: { - id: createdProductId, - input: { price: 89.99 } - }, - fields: ['id', 'price'] - }, { token: userToken }); // ← Creator: userAuth.user.id === product.createdBy - - expect(result.price).toBe(89.99); - }); - - it('should update product as admin', async () => { - const result = await testHelper.graphQl({ - name: 'updateProduct', - type: TestGraphQLType.MUTATION, - arguments: { - id: createdProductId, - input: { price: 79.99 } - }, - fields: ['id', 'price'] - }, { token: adminToken }); // ← Admin: adminAuth.user.roles contains 'admin' - - expect(result.price).toBe(79.99); - }); - - it('should fail to update product as non-creator', async () => { - const result = await testHelper.graphQl({ - name: 'updateProduct', - type: TestGraphQLType.MUTATION, - arguments: { - id: createdProductId, - input: { price: 69.99 } - }, - fields: ['id'] - }, { token: otherUserToken, statusCode: 403 }); // ← Not creator: otherUserAuth.user.id !== product.createdBy - - expect(result.errors).toBeDefined(); - }); - }); - - describe('Delete Product', () => { - it('should delete product as creator', async () => { - // First create a new product to delete - const created = await testHelper.graphQl({ - name: 'createProduct', - type: TestGraphQLType.MUTATION, - arguments: { input: { name: 'To Delete', price: 50 } }, - fields: ['id'] - }, { token: userToken }); - - // Delete as creator - const result = await testHelper.graphQl({ - name: 'deleteProduct', - type: TestGraphQLType.MUTATION, - arguments: { id: created.id }, - fields: ['id'] - }, { token: userToken }); // ← Creator: userAuth.user.id === created.createdBy - - expect(result.id).toBe(created.id); - }); - - it('should fail to delete product as non-creator', async () => { - const result = await testHelper.graphQl({ - name: 'deleteProduct', - type: TestGraphQLType.MUTATION, - arguments: { id: createdProductId }, - fields: ['id'] - }, { token: otherUserToken, statusCode: 403 }); // ← Not creator: otherUserAuth.user.id !== product.createdBy - - expect(result.errors).toBeDefined(); - }); - - it('should delete any product as admin', async () => { - const result = await testHelper.graphQl({ - name: 'deleteProduct', - type: TestGraphQLType.MUTATION, - arguments: { id: createdProductId }, - fields: ['id'] - }, { token: adminToken }); // ← Admin: adminAuth.user.roles contains 'admin' - - expect(result.id).toBe(createdProductId); - }); - }); -}); -``` - -**Permission Testing Checklist**: - -Before creating tests, verify: - -- [ ] I have checked the `@Roles()` decorators in controllers/resolvers -- [ ] I have checked the `@Restricted()` decorators in models/objects -- [ ] I have reviewed the `securityCheck()` logic in models -- [ ] I understand who can CREATE items (usually S_USER) -- [ ] I understand who can READ items (S_USER + securityCheck filtering) -- [ ] I understand who can UPDATE items (usually ADMIN + S_CREATOR) -- [ ] I understand who can DELETE items (usually ADMIN + S_CREATOR) -- [ ] I have created appropriate test users (admin, creator, non-creator) -- [ ] My tests use the CREATOR token (user.id === object.createdBy) for update/delete operations -- [ ] My tests verify that non-creators (user.id !== object.createdBy) CANNOT update/delete -- [ ] My tests verify that admins (user.roles contains 'admin') CAN update/delete everything - -**Common Permission Test Patterns**: - -1. **Test with creator** (user.id === object.createdBy) → Should succeed -2. **Test with admin** (user.roles contains 'admin') → Should succeed -3. **Test with other user** (user.id !== object.createdBy) → Should fail (403) - -**Only after understanding permissions, proceed to create tests.** - -#### Step 2.2: For Newly Created Modules - -**CRITICAL: Follow the correct test folder structure**: - -The project uses a specific test organization: - -1. **Module tests** (for modules in `src/server/modules/`): - ``` - tests/modules/.e2e-spec.ts - ``` - - Each module gets its own test file directly in `tests/modules/` - - Examples: `tests/modules/user.e2e-spec.ts`, `tests/modules/book.e2e-spec.ts` - -2. **Common tests** (for common functionality in `src/server/common/`): - ``` - tests/common.e2e-spec.ts - ``` - - All common functionality (enums, objects, helpers) tested here - - Single file for all common-related tests - -3. **Project tests** (for everything else - root level, config, etc.): - ``` - tests/project.e2e-spec.ts - ``` - - General project-level tests - - Configuration tests - - Integration tests - -**Determine correct test location**: - -```bash -# BEFORE creating a test, ask yourself: -# - Is this a module in src/server/modules/? → tests/modules/.e2e-spec.ts -# - Is this common functionality? → Add to tests/common.e2e-spec.ts -# - Is this project-level? → Add to tests/project.e2e-spec.ts - -# Check existing test structure to confirm: -ls -la tests/ -ls tests/modules/ -``` - -**Create new test files** for modules following the patterns you identified: - -```bash -# For a new module (e.g., Book) -tests/modules/book.e2e-spec.ts -``` - -**IMPORTANT**: Your new test file MUST: -1. **Match the exact structure** of existing test files -2. **Use the same imports** as existing tests -3. **Follow the same setup/cleanup pattern** (beforeAll, afterAll) -4. **Use the same test helpers/utilities** you observed -5. **Follow the same authentication pattern** -6. **Use the same assertion style** -7. **Follow the same naming conventions** for describe/it blocks - -**Each new test file must include**: -1. All CRUD operations (create, find all, find by ID, update, delete) -2. Authorization tests (unauthorized should fail, authorized should succeed) -3. Required field validation (missing required fields should fail) -4. Proper test data setup and cleanup (beforeAll, afterAll) -5. Tests for any custom methods or relationships - -**Ensure all prerequisites are met by analyzing existing tests**: - -Before writing test code, identify ALL prerequisites from existing test files: - -```bash -# Read an existing module test to understand prerequisites -cat tests/modules/user.e2e-spec.ts -``` - -**Common prerequisites to check**: -1. **Test data dependencies**: - - Does the module reference other modules? (e.g., Book → User for borrowedBy) - - Do you need to create related test data first? - - Example: To test Book with borrowedBy: User, create test User first - -2. **Authentication requirements**: - - What roles/permissions are needed? - - Do test users need to be created with specific roles? - - Example: Admin user for create operations, regular user for read operations - -3. **Database setup**: - - Are there database constraints or required collections? - - Do embedded objects or enums need to exist? - -4. **Configuration**: - - Are environment variables or config values needed? - - Example: JWT secrets, database connections - -**Pattern from existing tests**: -```typescript -// Example structure you should follow: -beforeAll(async () => { - // 1. Initialize test app/module - // 2. Set up database connection - // 3. Create prerequisite test data (users, roles, etc.) - // 4. Authenticate and get tokens -}); - -describe('Module Tests', () => { - // Tests here -}); - -afterAll(async () => { - // 1. Delete created test data (in reverse order) - // 2. Clean up connections - // 3. Close app -}); -``` - -**CRITICAL**: Look at how existing tests handle prerequisites and replicate the exact same approach. - -#### Step 2.3: For Modified Existing Modules - -**Update existing test files** when you modify modules: - -1. **FIRST: Read the existing test file completely**: - ```bash - # Find and read the test file for the module you modified - find tests -name "**.e2e-spec.ts" - cat tests/modules/.e2e-spec.ts - ``` - -2. **Understand what the existing tests cover**: - - Which operations are tested? - - Which properties are validated? - - What edge cases are covered? - - How is test data structured? - -3. **Run existing tests to ensure they pass BEFORE your changes**: - ```bash - npm run test:e2e - ``` - -4. **Review and update tests**: - - **Added properties**: Add tests verifying new properties work correctly - - **Changed validation**: Update tests to reflect new validation rules - - **Added relationships**: Add tests for new references/embedded objects - - **Changed required fields**: Update CreateInput tests accordingly - - **Removed properties**: Remove related test assertions - -5. **Verify test coverage**: - - All new properties are tested - - Changed behavior is verified - - Edge cases are covered - - Authorization still works correctly - -6. **Run tests again to ensure your changes don't break anything**: - ```bash - npm run test:e2e - ``` - -### Step 3: Compare with Existing Code - -**Compare generated code with existing project code**: - -1. **Read existing similar modules** to understand project patterns: - ```bash - # Example: If you created a User module, check existing modules - ls src/server/modules/ - ``` - -2. **Check for consistency**: - - Code style (indentation, spacing, formatting) - - Import ordering and organization - - Naming conventions (camelCase, PascalCase, kebab-case) - - File structure and directory organization - - Comment style and documentation - - Decorator usage (@Field, @Prop, etc.) - - Error handling patterns - - Validation patterns - -3. **Review property ordering**: - - Verify alphabetical order in models - - Verify alphabetical order in inputs - - Verify alphabetical order in outputs - - Check decorator consistency - -### Step 4: Critical Analysis - -**Analyze each file critically**: - -1. **Style consistency**: - - Does the code match the project's existing style? - - Are imports grouped and ordered correctly? - - Is indentation consistent with the project? - - Are naming conventions followed? - -2. **Structural consistency**: - - Are decorators in the same order as existing code? - - Is the file structure identical to existing modules? - - Are descriptions formatted the same way? - - Are relationships implemented consistently? - -3. **Code quality**: - - Are there any redundant imports? - - Are there any missing imports? - - Are descriptions meaningful and complete? - - Are TypeScript types correctly used? - -4. **Best practices**: - - Are required fields properly marked? - - Are nullable fields correctly configured? - - Are references properly typed? - - Are arrays correctly configured? - -### Step 5: Automated Optimizations - -**Apply automatic improvements**: - -1. **Fix import ordering**: - - External imports first (alphabetically) - - @lenne.tech/nest-server imports next - - Local imports last (alphabetically by path depth) - -2. **Fix property ordering**: - - Reorder all properties alphabetically in models - - Reorder all properties alphabetically in inputs - - Reorder all properties alphabetically in outputs - -3. **Fix formatting**: - - Ensure consistent indentation - - Remove extra blank lines - - Add missing blank lines between sections - -4. **Fix descriptions**: - - Ensure all follow "ENGLISH (DEUTSCH)" format - - Add missing descriptions - - Improve unclear descriptions - -5. **Fix common patterns**: - - Standardize decorator usage - - Standardize validation patterns - - Standardize error handling - -### Step 6: Pre-Report Testing - -**MANDATORY**: Run all tests before reporting: - -```bash -# Run TypeScript compilation -npm run build - -# Run linting -npm run lint - -# Run all tests -npm run test:e2e - -# If any fail, fix issues and repeat -``` - -**If tests fail**: -1. Analyze the error -2. Fix the issue -3. Re-run tests -4. Repeat until all tests pass - -#### Debugging Failed Tests - Important Guidelines - -**When tests fail, use systematic debugging with console.log statements:** - -1. **Add debug messages in Controllers/Resolvers**: - ```typescript - // In controller/resolver - BEFORE service call - console.log('🔵 [Controller] createProduct - Input:', input); - console.log('🔵 [Controller] createProduct - User:', serviceOptions?.user); - - const result = await this.productService.create(input, serviceOptions); - - // AFTER service call - console.log('🔵 [Controller] createProduct - Result:', result); - ``` - -2. **Add debug messages in Services**: - ```typescript - // In service method - console.log('🟢 [Service] create - Input:', input); - console.log('🟢 [Service] create - ServiceOptions:', serviceOptions); - - const created = await super.create(input, serviceOptions); - - console.log('🟢 [Service] create - Created:', created); - ``` - -3. **Understand the permissions system**: - - **Controllers/Resolvers**: `@Roles()` decorator controls WHO can call the endpoint - - **Services**: `serviceOptions.roles` controls what the service checks during processing - - **Models**: `securityCheck()` method determines what data is returned to the user - -4. **Default permission behavior**: - - Only **Admin users** (user.roles contains 'admin') OR the **creator** (user.id === object.createdBy) of an element can access it - - This is enforced in the `securityCheck()` method in models: - ```typescript - securityCheck(user: User, force?: boolean) { - // Admin: user.roles contains 'admin' - if (force || user?.hasRole(RoleEnum.ADMIN)) { - return this; // Admin sees everything - } - // Creator: user.id === this.createdBy - if (!equalIds(user, this.createdBy)) { - return undefined; // Non-creator (user.id !== this.createdBy) gets nothing - } - return this; // Creator sees their own data - } - ``` - -5. **Debugging strategy for permission issues**: - - **Step 1**: Run failing test with Admin user first - ```typescript - // In test setup - const adminToken = await testHelper.signIn('admin@test.com', 'admin-password'); - - // Use admin token in test - const result = await testHelper.graphQl({...}, { token: adminToken }); - ``` - - **Step 2**: Analyze results - - ✅ **Works with Admin, fails with normal user** → Permission issue (check Roles, securityCheck) - - ❌ **Fails with Admin too** → Different issue (check logic, data, validation) - -6. **Common permission issues and solutions**: - - | Problem | Cause | Solution | - |---------|-------|----------| - | 401/403 on endpoint | `@Roles()` too restrictive | Adjust decorator in controller/resolver | - | Empty result despite data existing | `securityCheck()` returns undefined | Modify securityCheck logic or use Admin | - | Service throws permission error | `serviceOptions.roles` check fails | Pass correct roles in serviceOptions | - -7. **Remove debug messages after fixing**: - ```bash - # After tests pass, remove all console.log statements - # Search for debug patterns - grep -r "console.log" src/server/modules/your-module/ - - # Remove them manually or with sed - # Then verify tests still pass - npm run test:e2e - ``` - -**Debugging workflow example**: -```typescript -// 1. Test fails - add debugging -@Mutation(() => Product) -async createProduct(@Args('input') input: ProductCreateInput, @GraphQLServiceOptions() opts) { - console.log('🔵 START createProduct', { input, user: opts?.user?.email }); - - const result = await this.productService.create(input, opts); - - console.log('🔵 END createProduct', { result: result?.id }); - return result; -} - -// In service -async create(input: ProductCreateInput, serviceOptions?: ServiceOptions) { - console.log('🟢 Service create', { input, user: serviceOptions?.user?.email }); - - const created = await super.create(input, serviceOptions); - - console.log('🟢 Service created', { id: created?.id, createdBy: created?.createdBy }); - return created; -} - -// 2. Run test - observe output: -// 🔵 START createProduct { input: {...}, user: 'test@test.com' } -// 🟢 Service create { input: {...}, user: 'test@test.com' } -// 🟢 Service created { id: '123', createdBy: '456' } -// 🔵 END createProduct { result: undefined } ← AHA! Result is undefined! - -// 3. Check model securityCheck() - likely returns undefined for non-creator (user.id !== object.createdBy) -// 4. Fix: Either use Admin user (user.roles contains 'admin') or adjust securityCheck logic -// 5. Test passes → Remove console.log statements -// 6. Verify tests still pass -``` - -**Do not proceed to final report if**: -- TypeScript compilation fails -- Linting fails -- Any tests fail -- Console shows errors or warnings - -### Step 7: Final Verification - -Before reporting, verify: - -- [ ] All files compared with existing code -- [ ] Code style matches project patterns -- [ ] All imports properly ordered -- [ ] All properties in alphabetical order -- [ ] All descriptions follow format -- [ ] **TestHelper source code read and understood** -- [ ] **TestHelper methods and configuration understood** -- [ ] **Existing tests analyzed BEFORE creating/modifying tests** -- [ ] **Existing tests passed BEFORE making changes** -- [ ] **Tests in correct location (tests/modules/.e2e-spec.ts, tests/common.e2e-spec.ts, or tests/project.e2e-spec.ts)** -- [ ] **New test files created for all new modules** -- [ ] **Existing test files updated for all modified modules** -- [ ] **All prerequisites identified and handled (test data dependencies, auth, etc.)** -- [ ] **All new/modified tests follow exact patterns from existing tests** -- [ ] TypeScript compiles without errors -- [ ] Linter passes without warnings -- [ ] **All tests pass AFTER changes** -- [ ] No console errors or warnings +**CRITICAL**: Before creating the final report, you MUST perform a comprehensive quality review. + +**📖 For the complete quality review process with all steps, checklists, and examples, see: `quality-review.md`** + +**Quick overview - 7 steps:** + +1. **Identify All Changes**: Use git to identify all created/modified files +2. **Test Management**: + - Analyze existing tests FIRST (understand TestHelper, patterns, permissions) + - Create new test files for newly created modules (in `tests/modules/`) + - Update existing test files for modified modules + - Follow exact patterns from existing tests +3. **Compare with Existing Code**: Check consistency (style, structure, naming) +4. **Critical Analysis**: Verify style, structure, code quality, best practices +5. **Automated Optimizations**: Fix import ordering, property ordering, formatting, descriptions +6. **Pre-Report Testing**: Run build, lint, and all tests (must all pass!) +7. **Final Verification**: Complete checklist before proceeding to Final Report + +**Critical reminders:** +- [ ] **TestHelper thoroughly understood** (read source code, understand graphQl() and rest() methods) +- [ ] **Existing tests analyzed** BEFORE creating new tests +- [ ] **Permission system understood** (3 layers: Controller @Roles, Service options, Model securityCheck) +- [ ] **Tests in correct location** (tests/modules/, tests/common.e2e-spec.ts, or tests/project.e2e-spec.ts) +- [ ] **All tests pass** before reporting + +**Permission testing approach:** +- Admin users: `user.roles.includes('admin')` +- Creators: `user.id === object.createdBy` +- Always test with appropriate user (creator for update/delete, admin for everything) +- Test security failures (403 responses) **Only after ALL checks pass, proceed to Final Report.** diff --git a/src/templates/claude-skills/nest-server-generator/configuration.md b/src/templates/claude-skills/nest-server-generator/configuration.md new file mode 100644 index 0000000..77438e3 --- /dev/null +++ b/src/templates/claude-skills/nest-server-generator/configuration.md @@ -0,0 +1,279 @@ +--- +name: nest-server-generator-configuration +version: 1.0.0 +description: Complete guide to lt.config.json configuration file +--- + +## Configuration File (lt.config.json) + +The lenne.tech CLI supports project-level configuration via `lt.config.json` files. This allows you to set default values for commands, eliminating the need for repeated CLI parameters or interactive prompts. + +### File Location and Hierarchy + +- **Location**: Place `lt.config.json` in your project root or any parent directory +- **Hierarchy**: The CLI searches from the current directory up to the root, merging configurations +- **Priority** (lowest to highest): + 1. Default values (hardcoded in CLI) + 2. Config from parent directories (higher up = lower priority) + 3. Config from current directory + 4. CLI parameters (`--flag value`) + 5. Interactive user input + +### Configuration Structure + +```json +{ + "meta": { + "version": "1.0.0", + "name": "My Project", + "description": "Optional project description" + }, + "commands": { + "server": { + "module": { + "controller": "Both", + "skipLint": false + }, + "object": { + "skipLint": false + }, + "addProp": { + "skipLint": false + } + } + } +} +``` + +### Available Configuration Options + +**Server Module Configuration (`commands.server.module`)**: +- `controller`: Default controller type (`"Rest"` | `"GraphQL"` | `"Both"` | `"auto"`) +- `skipLint`: Skip lint prompt after module creation (boolean) + +**Server Object Configuration (`commands.server.object`)**: +- `skipLint`: Skip lint prompt after object creation (boolean) + +**Server AddProp Configuration (`commands.server.addProp`)**: +- `skipLint`: Skip lint prompt after adding property (boolean) + +### Using Configuration in Commands + +**Example 1: Configure controller type globally** +```json +{ + "commands": { + "server": { + "module": { + "controller": "Rest" + } + } + } +} +``` + +Now all `lt server module` commands will default to REST controllers: +```bash +# Uses "Rest" from config (no prompt) +lt server module --name Product --prop-name-0 name --prop-type-0 string +``` + +**Example 2: Override config with CLI parameter** +```bash +# Ignores config, uses GraphQL +lt server module --name Product --controller GraphQL +``` + +**Example 3: Auto-detect from config** +```json +{ + "commands": { + "server": { + "module": { + "controller": "auto" + } + } + } +} +``` + +Now the CLI will auto-detect controller type from existing modules without prompting. + +### Managing Configuration + +**Initialize configuration**: +```bash +lt config init +``` + +**Show current configuration** (merged from all hierarchy levels): +```bash +lt config show +``` + +**Get help**: +```bash +lt config help +``` + +### When to Use Configuration + +**✅ Use configuration when:** +- Creating multiple modules with the same controller type +- Working in a team with agreed-upon conventions +- Automating module generation in CI/CD +- You want to skip repetitive prompts + +**❌ Don't use configuration when:** +- Creating a single module with specific requirements +- Each module needs a different controller type +- You're just testing or experimenting + +### Best Practices + +1. **Project Root**: Place `lt.config.json` in your project root +2. **Version Control**: Commit the config file to share with your team +3. **Documentation**: Add a README note explaining the config choices +4. **Override When Needed**: Use CLI parameters to override for special cases + +### 🎯 IMPORTANT: Configuration After Server Creation + +**CRITICAL WORKFLOW**: After creating a new server with `lt server create`, you **MUST** initialize the configuration file to set project conventions. + +#### Automatic Post-Creation Setup + +When you create a new NestJS server, immediately follow these steps: + +1. **Navigate to the API directory**: + ```bash + cd projects/api + ``` + +2. **Create the configuration file manually**: + ```bash + # Create lt.config.json with controller preference + ``` + +3. **Ask the developer for their preference** (if not already specified): + ``` + What controller type do you prefer for new modules in this project? + 1. Rest - REST controllers only + 2. GraphQL - GraphQL resolvers only + 3. Both - Both REST and GraphQL + 4. auto - Auto-detect from existing modules + ``` + +4. **Write the configuration** based on the answer: + ```json + { + "meta": { + "version": "1.0.0" + }, + "commands": { + "server": { + "module": { + "controller": "Rest" + } + } + } + } + ``` + +#### Why This Is Important + +- ✅ **Consistency**: All modules will follow the same pattern +- ✅ **No Prompts**: Developers won't be asked for controller type repeatedly +- ✅ **Team Alignment**: Everyone uses the same conventions +- ✅ **Automation**: Scripts and CI/CD can create modules without interaction + +#### Example Workflow + +```bash +# User creates new server +lt server create --name MyAPI + +# You (Claude) navigate to API directory +cd projects/api + +# You ask the user +"I've created the server. What controller type would you like to use for modules?" +"1. Rest (REST only)" +"2. GraphQL (GraphQL only)" +"3. Both (REST + GraphQL)" +"4. auto (Auto-detect)" + +# User answers: "Rest" + +# You create lt.config.json +{ + "meta": { + "version": "1.0.0" + }, + "commands": { + "server": { + "module": { + "controller": "Rest" + } + } + } +} + +# Confirm to user +"✅ Configuration saved! All new modules will default to REST controllers." +"You can change this anytime by editing lt.config.json or running 'lt config init'." +``` + +#### Configuration Options Explained + +**"Rest"**: +- ✅ Creates REST controllers (`@Controller()`) +- ❌ No GraphQL resolvers +- ❌ No PubSub integration +- **Best for**: Traditional REST APIs, microservices + +**"GraphQL"**: +- ❌ No REST controllers +- ✅ Creates GraphQL resolvers (`@Resolver()`) +- ✅ Includes PubSub for subscriptions +- **Best for**: GraphQL-first APIs, real-time apps + +**"Both"**: +- ✅ Creates REST controllers +- ✅ Creates GraphQL resolvers +- ✅ Includes PubSub +- **Best for**: Hybrid APIs, gradual migration + +**"auto"**: +- 🤖 Analyzes existing modules +- 🤖 Detects pattern automatically +- 🤖 No user prompt +- **Best for**: Following existing conventions + +#### When NOT to Create Config + +Skip config creation if: +- ❌ User is just testing/experimenting +- ❌ User explicitly says "no configuration" +- ❌ Project already has lt.config.json + +### Integration with Commands + +When generating code, **ALWAYS check for configuration**: +1. Load config via `lt config show` or check for `lt.config.json` +2. Use configured values in command construction +3. Only pass CLI parameters when overriding config + +**Example: Generating module with config** +```bash +# Check if config exists and what controller type is configured +# If config has "controller": "Rest", use it +lt server module --name Product --prop-name-0 name --prop-type-0 string + +# If config has "controller": "auto", let CLI detect +lt server module --name Order --prop-name-0 total --prop-type-0 number + +# Override config when needed +lt server module --name User --controller Both +``` + +## Command Syntax Reference diff --git a/src/templates/claude-skills/nest-server-generator/declare-keyword-warning.md b/src/templates/claude-skills/nest-server-generator/declare-keyword-warning.md new file mode 100644 index 0000000..639a273 --- /dev/null +++ b/src/templates/claude-skills/nest-server-generator/declare-keyword-warning.md @@ -0,0 +1,124 @@ +--- +name: nest-server-generator-declare-keyword +version: 1.0.0 +description: Critical warning about using the declare keyword in TypeScript classes +--- + +# 🚨 CRITICAL: NEVER USE `declare` KEYWORD FOR PROPERTIES + +**⚠️ IMPORTANT RULE: DO NOT use the `declare` keyword when defining properties in classes!** + +The `declare` keyword in TypeScript signals that a property is only a type declaration without a runtime value. This prevents decorators from being properly applied and overridden. + +--- + +## ❌ WRONG - Using `declare` + +```typescript +export class ProductCreateInput extends ProductInput { + declare name: string; // ❌ WRONG - Decorator won't be applied! + declare price: number; // ❌ WRONG - Decorator won't be applied! +} +``` + +--- + +## ✅ CORRECT - Without `declare` + +```typescript +export class ProductCreateInput extends ProductInput { + @UnifiedField({ description: 'Product name' }) + name: string; // ✅ CORRECT - Decorator works properly + + @UnifiedField({ description: 'Product price' }) + price: number; // ✅ CORRECT - Decorator works properly +} +``` + +--- + +## Why This Matters + +1. **Decorators require actual properties**: `@UnifiedField()`, `@Restricted()`, and other decorators need actual property declarations to attach metadata +2. **Override behavior**: When extending classes, using `declare` prevents decorators from being properly overridden +3. **Runtime behavior**: `declare` properties don't exist at runtime, breaking the decorator system + +--- + +## When You Might Be Tempted to Use `declare` + +- ❌ When extending a class and wanting to change a decorator +- ❌ When TypeScript shows "property is declared but never used" +- ❌ When dealing with inheritance and property redefinition + +--- + +## Correct Approach Instead + +Use the `override` keyword (when appropriate) but NEVER `declare`: + +```typescript +export class ProductCreateInput extends ProductInput { + // ✅ Use override when useDefineForClassFields is enabled + override name: string; + + // ✅ Apply decorators directly - they will override parent decorators + @UnifiedField({ description: 'Product name', isOptional: false }) + override price: number; +} +``` + +--- + +## Examples + +### ❌ WRONG - Using declare + +```typescript +// This will BREAK decorator functionality +export class AddressInput extends Address { + declare street: string; + declare city: string; + declare zipCode: string; +} +``` + +### ✅ CORRECT - Without declare + +```typescript +// This works properly +export class AddressInput extends Address { + @UnifiedField({ description: 'Street' }) + street: string; + + @UnifiedField({ description: 'City' }) + city: string; + + @UnifiedField({ description: 'Zip code' }) + zipCode: string; +} +``` + +### ✅ CORRECT - With override + +```typescript +// This also works when extending decorated properties +export class AddressInput extends Address { + @UnifiedField({ description: 'Street', isOptional: false }) + override street: string; + + @UnifiedField({ description: 'City', isOptional: false }) + override city: string; + + @UnifiedField({ description: 'Zip code', isOptional: true }) + override zipCode?: string; +} +``` + +--- + +## Remember + +**`declare` = no decorators = broken functionality!** + +Always use actual property declarations with decorators, optionally with the `override` keyword when extending classes. diff --git a/src/templates/claude-skills/nest-server-generator/description-management.md b/src/templates/claude-skills/nest-server-generator/description-management.md new file mode 100644 index 0000000..71616f1 --- /dev/null +++ b/src/templates/claude-skills/nest-server-generator/description-management.md @@ -0,0 +1,217 @@ +--- +name: nest-server-generator-description-management +version: 1.0.0 +description: Guidelines for consistent description management across all generated components +--- + +# 🚨 CRITICAL: Description Management + +**⚠️ COMMON MISTAKE:** Descriptions are often applied inconsistently or only partially. You MUST follow this process for EVERY component. + +--- + +## 🔍 Step 1: ALWAYS Extract Descriptions from User Input + +**BEFORE generating ANY code, scan the user's specification for description hints:** + +1. **Look for comments after `//`**: + ``` + Module: Product + - name: string // Product name + - price: number // Produktpreis + - stock?: number // Current stock level + ``` + +2. **Extract ALL comments** and store them for each property +3. **Identify language** (English or German) + +--- + +## 📝 Step 2: Format Descriptions Correctly + +**Rule**: `"ENGLISH_DESCRIPTION (DEUTSCHE_BESCHREIBUNG)"` + +### Processing Logic + +| User Input | Language | Formatted Description | +|------------|----------|----------------------| +| `// Product name` | English | `'Product name'` | +| `// Produktname` | German | `'Product name (Produktname)'` | +| `// Straße` | German | `'Street (Straße)'` | +| `// Postleizahl` (typo) | German | `'Postal code (Postleitzahl)'` | +| (no comment) | - | Create meaningful English description | + +### ⚠️ CRITICAL - Preserving Original Text + +**1. Fix spelling errors ONLY:** +- ✅ Correct typos: `Postleizahl` → `Postleitzahl` (missing 't') +- ✅ Fix character errors: `Starße` → `Straße` (wrong character) +- ✅ Correct English typos: `Prodcut name` → `Product name` + +**2. DO NOT change the wording:** +- ❌ NEVER rephrase: `Straße` → `Straßenname` (NO!) +- ❌ NEVER expand: `Produkt` → `Produktbezeichnung` (NO!) +- ❌ NEVER improve: `Name` → `Full name` (NO!) +- ❌ NEVER translate differently: `Name` → `Title` (NO!) + +**3. Why this is critical:** +- User comments may be **predefined terms** from requirements +- External systems may **reference these exact terms** +- Changing wording breaks **external integrations** + +### Examples + +``` +✅ CORRECT: +// Straße → 'Street (Straße)' (only translated) +// Starße → 'Street (Straße)' (typo fixed, then translated) +// Produkt → 'Product (Produkt)' (keep original word) +// Strasse → 'Street (Straße)' (ss→ß corrected, then translated) + +❌ WRONG: +// Straße → 'Street name (Straßenname)' (changed wording!) +// Produkt → 'Product name (Produktname)' (added word!) +// Name → 'Full name (Vollständiger Name)' (rephrased!) +``` + +**Rule Summary**: Fix typos, preserve wording, translate accurately. + +--- + +## ✅ Step 3: Apply Descriptions EVERYWHERE (Most Critical!) + +**🚨 YOU MUST apply the SAME description to ALL of these locations:** + +### For Module Properties + +**1. Model file** (`.model.ts`): +```typescript +@UnifiedField({ description: 'Product name (Produktname)' }) +name: string; +``` + +**2. Create Input** (`-create.input.ts`): +```typescript +@UnifiedField({ description: 'Product name (Produktname)' }) +name: string; +``` + +**3. Update Input** (`.input.ts`): +```typescript +@UnifiedField({ description: 'Product name (Produktname)' }) +name?: string; +``` + +### For SubObject Properties + +**1. Object file** (`.object.ts`): +```typescript +@UnifiedField({ description: 'Street (Straße)' }) +street: string; +``` + +**2. Object Create Input** (`-create.input.ts`): +```typescript +@UnifiedField({ description: 'Street (Straße)' }) +street: string; +``` + +**3. Object Update Input** (`.input.ts`): +```typescript +@UnifiedField({ description: 'Street (Straße)' }) +street?: string; +``` + +### For Object/Module Type Decorators + +Apply descriptions to the class decorators as well: + +```typescript +@ObjectType({ description: 'Address information (Adressinformationen)' }) +export class Address { ... } + +@InputType({ description: 'Address information (Adressinformationen)' }) +export class AddressInput { ... } + +@ObjectType({ description: 'Product entity (Produkt-Entität)' }) +export class Product extends CoreModel { ... } +``` + +--- + +## ⛔ Common Mistakes to AVOID + +1. ❌ **Partial application**: Descriptions only in Models, not in Inputs +2. ❌ **Inconsistent format**: German-only in some places, English-only in others +3. ❌ **Missing descriptions**: No descriptions when user provided comments +4. ❌ **Ignoring Object inputs**: Forgetting to add descriptions to SubObject Input files +5. ❌ **Wrong format**: Using `(ENGLISH)` instead of `ENGLISH (DEUTSCH)` +6. ❌ **Changing wording**: Rephrasing user's original terms +7. ❌ **Adding words**: Expanding user's terminology + +--- + +## ✅ Verification Checklist + +After generating code, ALWAYS verify: + +- [ ] All user comments/descriptions extracted from specification +- [ ] All descriptions follow format: `"ENGLISH (DEUTSCH)"` or `"ENGLISH"` +- [ ] Model properties have descriptions +- [ ] Create Input properties have SAME descriptions +- [ ] Update Input properties have SAME descriptions +- [ ] Object properties have descriptions +- [ ] Object Input properties have SAME descriptions +- [ ] Class-level `@ObjectType()` and `@InputType()` have descriptions +- [ ] NO German-only descriptions (must be translated) +- [ ] NO inconsistencies between files +- [ ] Original wording preserved (only typos fixed) + +--- + +## 🔄 If You Forget + +**If you generate code and realize descriptions are missing or inconsistent:** + +1. **STOP** - Don't continue with other phases +2. **Go back** and add/fix ALL descriptions +3. **Verify** using the checklist above +4. **Then continue** with remaining phases + +**Remember**: Descriptions are NOT optional "nice-to-have" - they are MANDATORY for: +- API documentation (Swagger/GraphQL) +- Code maintainability +- Developer experience +- Bilingual projects (German/English teams) + +--- + +## Quick Reference + +### Format Rules + +``` +English input → 'Product name' +German input → 'Product name (Produktname)' +No input → Create meaningful description +Typo input → Fix typo, then translate +Mixed input → Standardize to 'ENGLISH (DEUTSCH)' +``` + +### Application Checklist + +For **each property**: +- [ ] Model file +- [ ] Create Input file +- [ ] Update Input file + +For **each class**: +- [ ] @ObjectType() decorator +- [ ] @InputType() decorator (if applicable) + +### Remember + +- **Consistency is critical** - Same description everywhere +- **Preserve wording** - Only fix typos, never rephrase +- **Bilingual format** - Always use "ENGLISH (DEUTSCH)" for German terms +- **Verification** - Check all files before proceeding diff --git a/src/templates/claude-skills/nest-server-generator/examples.md b/src/templates/claude-skills/nest-server-generator/examples.md index 0372897..7069b17 100644 --- a/src/templates/claude-skills/nest-server-generator/examples.md +++ b/src/templates/claude-skills/nest-server-generator/examples.md @@ -1,6 +1,6 @@ --- name: nest-server-generator-examples -version: 1.0.0 +version: 1.0.1 description: Complete examples for generating NestJS server structures from specifications --- @@ -209,18 +209,144 @@ Manually update `book.input.ts` and `book-create.input.ts`: #### Step 5: Update Descriptions -Update all generated files to follow pattern: `"ENGLISH (DEUTSCH)"`: +**⚠️ CRITICAL STEP - Extract descriptions from original specification and apply EVERYWHERE!** + +**Step 5.1: Extract from specification** + +Go back to the original specification and identify ALL comments: + +``` +SubObject: Address +- street: string // Straße +- city: string // City name +- zipCode: string // Postleitzahl + +Module: Book +Model: Book +Extends: BaseItem +- isbn: string // ISBN-Nummer +- author: string // Author name +- publisher?: string // Verlag +``` + +**Extracted mapping:** +- Address.street → "Straße" (German) +- Address.city → "City name" (English) +- Address.zipCode → "Postleitzahl" (German) +- Book.isbn → "ISBN-Nummer" (German) +- Book.author → "Author name" (English) +- Book.publisher → "Verlag" (German) + +**Step 5.2: Format descriptions** + +Apply format rules: +- German → Translate and add: `ENGLISH (DEUTSCH)` +- English → Keep as-is +- **Fix typos only, preserve original wording!** + +``` +Address.street → 'Street (Straße)' (preserve word "Straße", don't change to "Straßenname") +Address.city → 'City name' +Address.zipCode → 'Postal code (Postleitzahl)' +Book.isbn → 'ISBN number (ISBN-Nummer)' +Book.author → 'Author name' +Book.publisher → 'Publisher (Verlag)' (preserve word "Verlag", don't expand) +``` + +**⚠️ CRITICAL - Preserve Original Wording:** + +User comments may be predefined terms or referenced by external systems. + +**Examples of correct handling**: +``` +✅ CORRECT: +// Straße → 'Street (Straße)' (just translate) +// Postleizahl → 'Postal code (Postleitzahl)' (fix typo, preserve word) +// Produkt → 'Product (Produkt)' (don't add "name") +// Status → 'Status (Status)' (same word) + +❌ WRONG: +// Straße → 'Street name (Straßenname)' (changed word!) +// Produkt → 'Product name (Produktname)' (added word!) +// Titel → 'Product title (Produkttitel)' (changed "Titel"!) +``` + +**Step 5.3: Apply to ALL files** + +For Address SubObject (3 files): ```typescript -// Before: -@UnifiedField({ description: 'ISBN number' }) +// File: src/server/common/objects/address/address.object.ts +@UnifiedField({ description: 'Street (Straße)' }) +street: string; + +@UnifiedField({ description: 'City name' }) +city: string; + +@UnifiedField({ description: 'Postal code (Postleitzahl)' }) +zipCode: string; + +// File: src/server/common/objects/address/address-create.input.ts +@UnifiedField({ description: 'Street (Straße)' }) +street: string; + +@UnifiedField({ description: 'City name' }) +city: string; + +@UnifiedField({ description: 'Postal code (Postleitzahl)' }) +zipCode: string; + +// File: src/server/common/objects/address/address.input.ts +@UnifiedField({ description: 'Street (Straße)' }) +street?: string; + +@UnifiedField({ description: 'City name' }) +city?: string; + +@UnifiedField({ description: 'Postal code (Postleitzahl)' }) +zipCode?: string; +``` + +For Book Module (3 files): + +```typescript +// File: src/server/modules/book/book.model.ts +@UnifiedField({ description: 'ISBN number (ISBN-Nummer)' }) isbn: string; -// After: +@UnifiedField({ description: 'Author name' }) +author: string; + +@UnifiedField({ description: 'Publisher (Verlag)' }) +publisher?: string; + +// File: src/server/modules/book/inputs/book-create.input.ts @UnifiedField({ description: 'ISBN number (ISBN-Nummer)' }) isbn: string; + +@UnifiedField({ description: 'Author name' }) +author: string; + +@UnifiedField({ description: 'Publisher (Verlag)' }) +publisher?: string; + +// File: src/server/modules/book/inputs/book.input.ts +@UnifiedField({ description: 'ISBN number (ISBN-Nummer)' }) +isbn?: string; + +@UnifiedField({ description: 'Author name' }) +author?: string; + +@UnifiedField({ description: 'Publisher (Verlag)' }) +publisher?: string; ``` +**Verification:** +- ✅ Same description in ALL 3 files for each property +- ✅ All German descriptions translated +- ✅ All user comments extracted and applied +- ✅ No inconsistencies + #### Step 6: Create Enum Files ```typescript diff --git a/src/templates/claude-skills/nest-server-generator/quality-review.md b/src/templates/claude-skills/nest-server-generator/quality-review.md new file mode 100644 index 0000000..6a051c2 --- /dev/null +++ b/src/templates/claude-skills/nest-server-generator/quality-review.md @@ -0,0 +1,855 @@ +--- +name: nest-server-generator-quality-review +version: 1.0.0 +description: Comprehensive quality review guidelines before creating final report +--- + +# Phase 8: Pre-Report Quality Review + +**CRITICAL**: Before creating the final report, you MUST perform a comprehensive quality review: + +## Step 1: Identify All Changes + +Use git to identify all created and modified files: + +```bash +git status --short +git diff --name-only +``` + +For each file, review: +- All newly created files +- All modified files +- File structure and organization + +## Step 2: Test Management + +**CRITICAL**: Ensure tests are created/updated for all changes: + +### Step 2.1: Analyze Existing Tests FIRST + +**BEFORE creating or modifying ANY tests, you MUST thoroughly analyze existing tests**: + +1. **Identify all existing test files**: + ```bash + # List all test directories and files + ls -la tests/ + ls -la tests/modules/ + find tests -name "*.e2e-spec.ts" -type f + ``` + +2. **Read multiple existing test files completely**: + ```bash + # Read at least 2-3 different module tests to understand patterns + cat tests/modules/user.e2e-spec.ts + cat tests/modules/.e2e-spec.ts + + # Also check the common and project test files + cat tests/common.e2e-spec.ts + cat tests/project.e2e-spec.ts + ``` + +3. **CRITICAL: Understand the TestHelper thoroughly**: + + **Before creating any tests, you MUST understand the TestHelper from @lenne.tech/nest-server**: + + ```bash + # Read the TestHelper source code to understand its capabilities + cat node_modules/@lenne.tech/nest-server/src/test/test.helper.ts + ``` + + **Analyze the TestHelper to understand**: + - **Available methods**: What methods does TestHelper provide? + - **Configuration options**: How can TestHelper be configured? + - **GraphQL support**: How to use `graphQl()` method? What parameters does it accept? + - **REST support**: How to use `rest()` method? What parameters does it accept? + - **Authentication**: How does TestHelper handle tokens and authentication? + - **Request building**: How are requests constructed? What options are available? + - **Response handling**: How are responses processed? What format is returned? + - **Error handling**: How does TestHelper handle errors and failures? + - **Helper utilities**: What additional utilities are available? + + **Document your findings**: + ```typescript + // Example: Understanding TestHelper.graphQl() + // Method signature: graphQl(options: GraphQLOptions, config?: RequestConfig) + // GraphQLOptions: { name, type (QUERY/MUTATION), arguments, fields } + // RequestConfig: { token, statusCode, headers } + // Returns: Parsed response data or error + + // Example: Understanding TestHelper.rest() + // Method signature: rest(method: HttpMethod, path: string, options?: RestOptions) + // HttpMethod: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' + // RestOptions: { body, token, statusCode, headers } + // Returns: Response data or error + ``` + + **Common TestHelper patterns to understand**: + - How to execute GraphQL queries/mutations with `graphQl()` + - How to execute REST requests with `rest()` + - How to pass authentication tokens (same for both methods) + - How to handle expected errors (statusCode parameter) + - How to work with response data + - How to structure test data + - When to use GraphQL vs REST methods + + **Only after fully understanding TestHelper, proceed to next step.** + +4. **Understand the testing approach used**: + - Which test framework? (Jest, Mocha, etc.) + - Which testing utilities? (@lenne.tech/nest-server testHelper, custom helpers) + - How is the test app initialized? (beforeAll setup) + - How are test users/auth handled? + - How is test data created and cleaned up? + - What assertion library? (expect, should, etc.) + - Are there custom matchers? + +5. **Document the patterns you observe**: + - **Import patterns**: Which modules are imported? In what order? + - **Setup patterns**: How is beforeAll/beforeEach structured? + - **Auth patterns**: How do tests authenticate? Token handling? + - **Test structure**: Describe blocks organization? Test naming conventions? + - **CRUD patterns**: How are create/read/update/delete tested? + - **Assertion patterns**: What assertions are used? How detailed? + - **Cleanup patterns**: How is afterAll/afterEach structured? + - **Error testing**: How are failures/validations tested? + +6. **Verify existing tests run successfully**: + ```bash + # Run existing tests to ensure they pass + npm run test:e2e + + # If any fail, understand why before proceeding + # Your new/modified tests MUST NOT break existing tests + ``` + +7. **Create a mental checklist**: + - [ ] I have read and understand the TestHelper source code + - [ ] I understand TestHelper methods and configuration + - [ ] I understand how to use graphQl() method (GraphQL queries/mutations) + - [ ] I understand how to use rest() method (REST endpoints) + - [ ] I understand when to use graphQl() vs rest() + - [ ] I understand TestHelper authentication and error handling + - [ ] I understand which test helpers/utilities are used + - [ ] I understand the authentication/authorization pattern + - [ ] I understand the test data lifecycle (create/cleanup) + - [ ] I understand the assertion patterns + - [ ] I understand the error testing approach + - [ ] All existing tests pass before I make changes + +**Only after completing this analysis, proceed to create or modify tests.** + +### Step 2.1.1: Understanding Permissions and User Rights in Tests + +**CRITICAL**: Before creating tests, you MUST understand the 3-layer permission system: + +**Important Definitions**: + +- **Admin User**: A user whose `roles` array contains `'admin'` + ```typescript + // Example admin user + { + id: '123', + email: 'admin@test.com', + roles: ['admin', 'user'] // ← Contains 'admin' + } + ``` + +- **Creator**: The user who created an object, identified by matching IDs + ```typescript + // User who created the object + const user = { id: 'user-123', email: 'creator@test.com' }; + + // Object created by this user + const product = { + id: 'product-456', + name: 'Test Product', + createdBy: 'user-123' // ← Matches user.id → This user is the CREATOR + }; + + // Different user (NOT the creator) + const otherUser = { id: 'user-789', email: 'other@test.com' }; + // otherUser.id !== product.createdBy → NOT the creator! + ``` + +**The Three Permission Layers**: + +1. **Controller/Resolver Layer** (`@Roles()` decorator): + - Controls WHO can call the endpoint + - Example: `@Roles(RoleEnum.ADMIN)` → Only admins can call this endpoint + - Example: `@Roles(RoleEnum.S_USER)` → All signed-in users can call + +2. **Service Layer** (`serviceOptions.roles` parameter): + - Controls what permissions are checked during service processing + - Example: Update/Delete often require `[RoleEnum.ADMIN, RoleEnum.S_CREATOR]` + - The creator can update/delete their own items + +3. **Model Layer** (`securityCheck()` method): + - Controls WHAT data is returned to the user + - Standard implementation: + ```typescript + securityCheck(user: User, force?: boolean) { + // Admins see everything (user.roles contains 'admin') + if (force || user?.hasRole(RoleEnum.ADMIN)) { + return this; + } + // Only creator can see their own data (user.id === this.createdBy) + if (!equalIds(user, this.createdBy)) { + return undefined; // Non-creator gets nothing! + } + return this; + } + ``` + - **Key checks**: + - `user?.hasRole(RoleEnum.ADMIN)` → Returns `true` if `user.roles.includes('admin')` + - `equalIds(user, this.createdBy)` → Returns `true` if `user.id === this.createdBy` + +**Default Permission Behavior**: +- **Create**: Usually accessible to signed-in users (`RoleEnum.S_USER`) +- **Read/List**: Usually accessible to signed-in users, but securityCheck filters results +- **Update**: Only ADMIN or CREATOR (via `serviceOptions.roles` check) +- **Delete**: Only ADMIN or CREATOR (via `serviceOptions.roles` check) + +**Analyzing Permissions Before Creating Tests**: + +Before writing tests, check these 3 locations: + +1. **Check Controller/Resolver decorators**: + ```typescript + // In product.resolver.ts + @Roles(RoleEnum.ADMIN) // ← WHO can call this? + @Query(() => Product) + async getProduct(@Args('id') id: string) { ... } + + @Roles(RoleEnum.S_USER) // ← All signed-in users + @Mutation(() => Product) + async createProduct(@Args('input') input: ProductCreateInput) { ... } + ``` + +2. **Check Model/Object `@Restricted` decorators**: + ```typescript + // In product.model.ts + @Restricted(RoleEnum.ADMIN) // ← Model-level restriction + export class Product extends CoreModel { + + @Restricted(RoleEnum.ADMIN) // ← Property-level restriction + @UnifiedField() + internalNotes?: string; + } + ``` + +3. **Check Model `securityCheck()` logic**: + ```typescript + // In product.model.ts + securityCheck(user: User, force?: boolean) { + // Admin check: user.roles contains 'admin' + if (force || user?.hasRole(RoleEnum.ADMIN)) { + return this; // Admin sees all + } + + // Custom logic: Allow public products for everyone + if (this.isPublic) { + return this; + } + + // Creator check: user.id === this.createdBy + if (!equalIds(user, this.createdBy)) { + return undefined; // Non-creator gets nothing + } + return this; // Creator sees their own product + } + ``` + +**Creating Appropriate Test Users**: + +Based on permission analysis, create appropriate test users: + +```typescript +describe('Product Module', () => { + let testHelper: TestHelper; + let adminToken: string; + let userToken: string; + let otherUserToken: string; + let createdProductId: string; + + beforeAll(async () => { + testHelper = new TestHelper(app); + + // Admin user (user.roles contains 'admin') + const adminAuth = await testHelper.graphQl({ + name: 'signIn', + type: TestGraphQLType.MUTATION, + arguments: { email: 'admin@test.com', password: 'admin' }, + fields: ['token', 'user { id email roles }'] + }); + adminToken = adminAuth.token; + // adminAuth.user.roles = ['admin', 'user'] ← Contains 'admin' + + // Regular user (will be the creator of test objects) + const userAuth = await testHelper.graphQl({ + name: 'signIn', + type: TestGraphQLType.MUTATION, + arguments: { email: 'user@test.com', password: 'user' }, + fields: ['token', 'user { id email roles }'] + }); + userToken = userAuth.token; + // When this user creates an object → object.createdBy = userAuth.user.id + + // Another regular user (will NOT be the creator) + const otherUserAuth = await testHelper.graphQl({ + name: 'signIn', + type: TestGraphQLType.MUTATION, + arguments: { email: 'other@test.com', password: 'other' }, + fields: ['token', 'user { id email roles }'] + }); + otherUserToken = otherUserAuth.token; + // otherUserAuth.user.id !== object.createdBy → NOT the creator + }); +}); +``` + +**Test Structure Based on Permissions**: + +```typescript +describe('Product Module', () => { + // ... setup with adminToken, userToken, otherUserToken + + describe('Create Product', () => { + it('should create product as regular user', async () => { + const result = await testHelper.graphQl({ + name: 'createProduct', + type: TestGraphQLType.MUTATION, + arguments: { input: { name: 'Test Product', price: 99.99 } }, + fields: ['id', 'name', 'createdBy { id }'] + }, { token: userToken }); // ← Created by userToken + + expect(result.name).toBe('Test Product'); + // result.createdBy.id now equals userAuth.user.id + // → userToken is the CREATOR of this product + createdProductId = result.id; + }); + }); + + describe('Update Product', () => { + it('should update product as creator', async () => { + const result = await testHelper.graphQl({ + name: 'updateProduct', + type: TestGraphQLType.MUTATION, + arguments: { + id: createdProductId, + input: { price: 89.99 } + }, + fields: ['id', 'price'] + }, { token: userToken }); // ← Creator: userAuth.user.id === product.createdBy + + expect(result.price).toBe(89.99); + }); + + it('should update product as admin', async () => { + const result = await testHelper.graphQl({ + name: 'updateProduct', + type: TestGraphQLType.MUTATION, + arguments: { + id: createdProductId, + input: { price: 79.99 } + }, + fields: ['id', 'price'] + }, { token: adminToken }); // ← Admin: adminAuth.user.roles contains 'admin' + + expect(result.price).toBe(79.99); + }); + + it('should fail to update product as non-creator', async () => { + const result = await testHelper.graphQl({ + name: 'updateProduct', + type: TestGraphQLType.MUTATION, + arguments: { + id: createdProductId, + input: { price: 69.99 } + }, + fields: ['id'] + }, { token: otherUserToken, statusCode: 403 }); // ← Not creator: otherUserAuth.user.id !== product.createdBy + + expect(result.errors).toBeDefined(); + }); + }); + + describe('Delete Product', () => { + it('should delete product as creator', async () => { + // First create a new product to delete + const created = await testHelper.graphQl({ + name: 'createProduct', + type: TestGraphQLType.MUTATION, + arguments: { input: { name: 'To Delete', price: 50 } }, + fields: ['id'] + }, { token: userToken }); + + // Delete as creator + const result = await testHelper.graphQl({ + name: 'deleteProduct', + type: TestGraphQLType.MUTATION, + arguments: { id: created.id }, + fields: ['id'] + }, { token: userToken }); // ← Creator: userAuth.user.id === created.createdBy + + expect(result.id).toBe(created.id); + }); + + it('should fail to delete product as non-creator', async () => { + const result = await testHelper.graphQl({ + name: 'deleteProduct', + type: TestGraphQLType.MUTATION, + arguments: { id: createdProductId }, + fields: ['id'] + }, { token: otherUserToken, statusCode: 403 }); // ← Not creator: otherUserAuth.user.id !== product.createdBy + + expect(result.errors).toBeDefined(); + }); + + it('should delete any product as admin', async () => { + const result = await testHelper.graphQl({ + name: 'deleteProduct', + type: TestGraphQLType.MUTATION, + arguments: { id: createdProductId }, + fields: ['id'] + }, { token: adminToken }); // ← Admin: adminAuth.user.roles contains 'admin' + + expect(result.id).toBe(createdProductId); + }); + }); +}); +``` + +**Permission Testing Checklist**: + +Before creating tests, verify: + +- [ ] I have checked the `@Roles()` decorators in controllers/resolvers +- [ ] I have checked the `@Restricted()` decorators in models/objects +- [ ] I have reviewed the `securityCheck()` logic in models +- [ ] I understand who can CREATE items (usually S_USER) +- [ ] I understand who can READ items (S_USER + securityCheck filtering) +- [ ] I understand who can UPDATE items (usually ADMIN + S_CREATOR) +- [ ] I understand who can DELETE items (usually ADMIN + S_CREATOR) +- [ ] I have created appropriate test users (admin, creator, non-creator) +- [ ] My tests use the CREATOR token (user.id === object.createdBy) for update/delete operations +- [ ] My tests verify that non-creators (user.id !== object.createdBy) CANNOT update/delete +- [ ] My tests verify that admins (user.roles contains 'admin') CAN update/delete everything + +**Common Permission Test Patterns**: + +1. **Test with creator** (user.id === object.createdBy) → Should succeed +2. **Test with admin** (user.roles contains 'admin') → Should succeed +3. **Test with other user** (user.id !== object.createdBy) → Should fail (403) + +**Only after understanding permissions, proceed to create tests.** + +### Step 2.2: For Newly Created Modules + +**CRITICAL: Follow the correct test folder structure**: + +The project uses a specific test organization: + +1. **Module tests** (for modules in `src/server/modules/`): + ``` + tests/modules/.e2e-spec.ts + ``` + - Each module gets its own test file directly in `tests/modules/` + - Examples: `tests/modules/user.e2e-spec.ts`, `tests/modules/book.e2e-spec.ts` + +2. **Common tests** (for common functionality in `src/server/common/`): + ``` + tests/common.e2e-spec.ts + ``` + - All common functionality (enums, objects, helpers) tested here + - Single file for all common-related tests + +3. **Project tests** (for everything else - root level, config, etc.): + ``` + tests/project.e2e-spec.ts + ``` + - General project-level tests + - Configuration tests + - Integration tests + +**Determine correct test location**: + +```bash +# BEFORE creating a test, ask yourself: +# - Is this a module in src/server/modules/? → tests/modules/.e2e-spec.ts +# - Is this common functionality? → Add to tests/common.e2e-spec.ts +# - Is this project-level? → Add to tests/project.e2e-spec.ts + +# Check existing test structure to confirm: +ls -la tests/ +ls tests/modules/ +``` + +**Create new test files** for modules following the patterns you identified: + +```bash +# For a new module (e.g., Book) +tests/modules/book.e2e-spec.ts +``` + +**IMPORTANT**: Your new test file MUST: +1. **Match the exact structure** of existing test files +2. **Use the same imports** as existing tests +3. **Follow the same setup/cleanup pattern** (beforeAll, afterAll) +4. **Use the same test helpers/utilities** you observed +5. **Follow the same authentication pattern** +6. **Use the same assertion style** +7. **Follow the same naming conventions** for describe/it blocks + +**Each new test file must include**: +1. All CRUD operations (create, find all, find by ID, update, delete) +2. Authorization tests (unauthorized should fail, authorized should succeed) +3. Required field validation (missing required fields should fail) +4. Proper test data setup and cleanup (beforeAll, afterAll) +5. Tests for any custom methods or relationships + +**Ensure all prerequisites are met by analyzing existing tests**: + +Before writing test code, identify ALL prerequisites from existing test files: + +```bash +# Read an existing module test to understand prerequisites +cat tests/modules/user.e2e-spec.ts +``` + +**Common prerequisites to check**: +1. **Test data dependencies**: + - Does the module reference other modules? (e.g., Book → User for borrowedBy) + - Do you need to create related test data first? + - Example: To test Book with borrowedBy: User, create test User first + +2. **Authentication requirements**: + - What roles/permissions are needed? + - Do test users need to be created with specific roles? + - Example: Admin user for create operations, regular user for read operations + +3. **Database setup**: + - Are there database constraints or required collections? + - Do embedded objects or enums need to exist? + +4. **Configuration**: + - Are environment variables or config values needed? + - Example: JWT secrets, database connections + +**Pattern from existing tests**: +```typescript +// Example structure you should follow: +beforeAll(async () => { + // 1. Initialize test app/module + // 2. Set up database connection + // 3. Create prerequisite test data (users, roles, etc.) + // 4. Authenticate and get tokens +}); + +describe('Module Tests', () => { + // Tests here +}); + +afterAll(async () => { + // 1. Delete created test data (in reverse order) + // 2. Clean up connections + // 3. Close app +}); +``` + +**CRITICAL**: Look at how existing tests handle prerequisites and replicate the exact same approach. + +### Step 2.3: For Modified Existing Modules + +**Update existing test files** when you modify modules: + +1. **FIRST: Read the existing test file completely**: + ```bash + # Find and read the test file for the module you modified + find tests -name "**.e2e-spec.ts" + cat tests/modules/.e2e-spec.ts + ``` + +2. **Understand what the existing tests cover**: + - Which operations are tested? + - Which properties are validated? + - What edge cases are covered? + - How is test data structured? + +3. **Run existing tests to ensure they pass BEFORE your changes**: + ```bash + npm run test:e2e + ``` + +4. **Review and update tests**: + - **Added properties**: Add tests verifying new properties work correctly + - **Changed validation**: Update tests to reflect new validation rules + - **Added relationships**: Add tests for new references/embedded objects + - **Changed required fields**: Update CreateInput tests accordingly + - **Removed properties**: Remove related test assertions + +5. **Verify test coverage**: + - All new properties are tested + - Changed behavior is verified + - Edge cases are covered + - Authorization still works correctly + +6. **Run tests again to ensure your changes don't break anything**: + ```bash + npm run test:e2e + ``` + +## Step 3: Compare with Existing Code + +**Compare generated code with existing project code**: + +1. **Read existing similar modules** to understand project patterns: + ```bash + # Example: If you created a User module, check existing modules + ls src/server/modules/ + ``` + +2. **Check for consistency**: + - Code style (indentation, spacing, formatting) + - Import ordering and organization + - Naming conventions (camelCase, PascalCase, kebab-case) + - File structure and directory organization + - Comment style and documentation + - Decorator usage (@Field, @Prop, etc.) + - Error handling patterns + - Validation patterns + +3. **Review property ordering**: + - Verify alphabetical order in models + - Verify alphabetical order in inputs + - Verify alphabetical order in outputs + - Check decorator consistency + +## Step 4: Critical Analysis + +**Analyze each file critically**: + +1. **Style consistency**: + - Does the code match the project's existing style? + - Are imports grouped and ordered correctly? + - Is indentation consistent with the project? + - Are naming conventions followed? + +2. **Structural consistency**: + - Are decorators in the same order as existing code? + - Is the file structure identical to existing modules? + - Are descriptions formatted the same way? + - Are relationships implemented consistently? + +3. **Code quality**: + - Are there any redundant imports? + - Are there any missing imports? + - Are descriptions meaningful and complete? + - Are TypeScript types correctly used? + +4. **Best practices**: + - Are required fields properly marked? + - Are nullable fields correctly configured? + - Are references properly typed? + - Are arrays correctly configured? + +## Step 5: Automated Optimizations + +**Apply automatic improvements**: + +1. **Fix import ordering**: + - External imports first (alphabetically) + - @lenne.tech/nest-server imports next + - Local imports last (alphabetically by path depth) + +2. **Fix property ordering**: + - Reorder all properties alphabetically in models + - Reorder all properties alphabetically in inputs + - Reorder all properties alphabetically in outputs + +3. **Fix formatting**: + - Ensure consistent indentation + - Remove extra blank lines + - Add missing blank lines between sections + +4. **Fix descriptions**: + - Ensure all follow "ENGLISH (DEUTSCH)" format + - Add missing descriptions + - Improve unclear descriptions + +5. **Fix common patterns**: + - Standardize decorator usage + - Standardize validation patterns + - Standardize error handling + +## Step 6: Pre-Report Testing + +**MANDATORY**: Run all tests before reporting: + +```bash +# Run TypeScript compilation +npm run build + +# Run linting +npm run lint + +# Run all tests +npm run test:e2e + +# If any fail, fix issues and repeat +``` + +**If tests fail**: +1. Analyze the error +2. Fix the issue +3. Re-run tests +4. Repeat until all tests pass + +### Debugging Failed Tests - Important Guidelines + +**When tests fail, use systematic debugging with console.log statements:** + +1. **Add debug messages in Controllers/Resolvers**: + ```typescript + // In controller/resolver - BEFORE service call + console.log('🔵 [Controller] createProduct - Input:', input); + console.log('🔵 [Controller] createProduct - User:', serviceOptions?.user); + + const result = await this.productService.create(input, serviceOptions); + + // AFTER service call + console.log('🔵 [Controller] createProduct - Result:', result); + ``` + +2. **Add debug messages in Services**: + ```typescript + // In service method + console.log('🟢 [Service] create - Input:', input); + console.log('🟢 [Service] create - ServiceOptions:', serviceOptions); + + const created = await super.create(input, serviceOptions); + + console.log('🟢 [Service] create - Created:', created); + ``` + +3. **Understand the permissions system**: + - **Controllers/Resolvers**: `@Roles()` decorator controls WHO can call the endpoint + - **Services**: `serviceOptions.roles` controls what the service checks during processing + - **Models**: `securityCheck()` method determines what data is returned to the user + +4. **Default permission behavior**: + - Only **Admin users** (user.roles contains 'admin') OR the **creator** (user.id === object.createdBy) of an element can access it + - This is enforced in the `securityCheck()` method in models: + ```typescript + securityCheck(user: User, force?: boolean) { + // Admin: user.roles contains 'admin' + if (force || user?.hasRole(RoleEnum.ADMIN)) { + return this; // Admin sees everything + } + // Creator: user.id === this.createdBy + if (!equalIds(user, this.createdBy)) { + return undefined; // Non-creator (user.id !== this.createdBy) gets nothing + } + return this; // Creator sees their own data + } + ``` + +5. **Debugging strategy for permission issues**: + + **Step 1**: Run failing test with Admin user first + ```typescript + // In test setup + const adminToken = await testHelper.signIn('admin@test.com', 'admin-password'); + + // Use admin token in test + const result = await testHelper.graphQl({...}, { token: adminToken }); + ``` + + **Step 2**: Analyze results + - ✅ **Works with Admin, fails with normal user** → Permission issue (check Roles, securityCheck) + - ❌ **Fails with Admin too** → Different issue (check logic, data, validation) + +6. **Common permission issues and solutions**: + + | Problem | Cause | Solution | + |---------|-------|----------| + | 401/403 on endpoint | `@Roles()` too restrictive | Adjust decorator in controller/resolver | + | Empty result despite data existing | `securityCheck()` returns undefined | Modify securityCheck logic or use Admin | + | Service throws permission error | `serviceOptions.roles` check fails | Pass correct roles in serviceOptions | + +7. **Remove debug messages after fixing**: + ```bash + # After tests pass, remove all console.log statements + # Search for debug patterns + grep -r "console.log" src/server/modules/your-module/ + + # Remove them manually or with sed + # Then verify tests still pass + npm run test:e2e + ``` + +**Debugging workflow example**: +```typescript +// 1. Test fails - add debugging +@Mutation(() => Product) +async createProduct(@Args('input') input: ProductCreateInput, @GraphQLServiceOptions() opts) { + console.log('🔵 START createProduct', { input, user: opts?.user?.email }); + + const result = await this.productService.create(input, opts); + + console.log('🔵 END createProduct', { result: result?.id }); + return result; +} + +// In service +async create(input: ProductCreateInput, serviceOptions?: ServiceOptions) { + console.log('🟢 Service create', { input, user: serviceOptions?.user?.email }); + + const created = await super.create(input, serviceOptions); + + console.log('🟢 Service created', { id: created?.id, createdBy: created?.createdBy }); + return created; +} + +// 2. Run test - observe output: +// 🔵 START createProduct { input: {...}, user: 'test@test.com' } +// 🟢 Service create { input: {...}, user: 'test@test.com' } +// 🟢 Service created { id: '123', createdBy: '456' } +// 🔵 END createProduct { result: undefined } ← AHA! Result is undefined! + +// 3. Check model securityCheck() - likely returns undefined for non-creator (user.id !== object.createdBy) +// 4. Fix: Either use Admin user (user.roles contains 'admin') or adjust securityCheck logic +// 5. Test passes → Remove console.log statements +// 6. Verify tests still pass +``` + +**Do not proceed to final report if**: +- TypeScript compilation fails +- Linting fails +- Any tests fail +- Console shows errors or warnings + +## Step 7: Final Verification + +Before reporting, verify: + +- [ ] All files compared with existing code +- [ ] Code style matches project patterns +- [ ] All imports properly ordered +- [ ] All properties in alphabetical order +- [ ] All descriptions follow format +- [ ] **TestHelper source code read and understood** +- [ ] **TestHelper methods and configuration understood** +- [ ] **Existing tests analyzed BEFORE creating/modifying tests** +- [ ] **Existing tests passed BEFORE making changes** +- [ ] **Tests in correct location (tests/modules/.e2e-spec.ts, tests/common.e2e-spec.ts, or tests/project.e2e-spec.ts)** +- [ ] **New test files created for all new modules** +- [ ] **Existing test files updated for all modified modules** +- [ ] **All prerequisites identified and handled (test data dependencies, auth, etc.)** +- [ ] **All new/modified tests follow exact patterns from existing tests** +- [ ] TypeScript compiles without errors +- [ ] Linter passes without warnings +- [ ] **All tests pass AFTER changes** +- [ ] No console errors or warnings + +**Only after ALL checks pass, proceed to Final Report.** diff --git a/src/templates/claude-skills/nest-server-generator/reference.md b/src/templates/claude-skills/nest-server-generator/reference.md index 6e56657..c22970b 100644 --- a/src/templates/claude-skills/nest-server-generator/reference.md +++ b/src/templates/claude-skills/nest-server-generator/reference.md @@ -1,6 +1,6 @@ --- name: nest-server-generator-reference -version: 1.0.0 +version: 1.0.1 description: Quick reference for ALL NestJS server development - from simple single commands to complex structure generation --- @@ -63,7 +63,13 @@ Use this skill for **ANY** NestJS/nest-server work, no matter how simple or comp ☐ 4. Create all Objects ☐ 5. Create all Modules (dependency order) ☐ 6. Handle inheritance (manual edits) -☐ 7. Update all descriptions (ENGLISH (DEUTSCH)) +☐ 7. Update ALL descriptions EVERYWHERE (CRITICAL!) + ☐ 7.1. Extract ALL user comments (after //) from specification + ☐ 7.2. Format descriptions: ENGLISH (DEUTSCH) + ☐ 7.3. Apply to ALL Module files (Model, CreateInput, UpdateInput) + ☐ 7.4. Apply to ALL SubObject files (Object, CreateInput, UpdateInput) + ☐ 7.5. Add to ALL class decorators (@ObjectType, @InputType) + ☐ 7.6. Verify consistency (same property = same description) ☐ 8. Alphabetize all properties ☐ 9. Create enum files ☐ 10. Create API tests @@ -140,20 +146,57 @@ lt server addProp --type Module --element \ ## Description Format +**⚠️ CRITICAL:** Always extract descriptions from user comments (after `//`) and apply EVERYWHERE! + **Rule**: `"ENGLISH_DESCRIPTION (DEUTSCHE_BESCHREIBUNG)"` ### Processing -| Input Comment | Output Description | -|---------------|-------------------| -| `// Street name` | `'Street name'` | -| `// Straße` | `'Street (Straße)'` | -| (no comment) | Create meaningful English description | - -### Apply To -- Model property `@UnifiedField({ description: '...' })` -- Input property `@UnifiedField({ description: '...' })` -- Output property `@UnifiedField({ description: '...' })` +| Input Comment | Language | Output Description | +|---------------|----------|-------------------| +| `// Product name` | English | `'Product name'` | +| `// Produktname` | German | `'Product name (Produktname)'` | +| `// Street name` | English | `'Street name'` | +| `// Straße` | German | `'Street (Straße)'` | +| `// Postleizahl` (typo) | German | `'Postal code (Postleitzahl)'` (corrected) | +| (no comment) | - | Create meaningful English description | + +**⚠️ Preserve Original Wording:** +- ✅ Fix typos: `Postleizahl` → `Postleitzahl`, `Starße` → `Straße` +- ❌ DON'T rephrase: `Straße` → `Straßenname` (NO!) +- ❌ DON'T expand: `Produkt` → `Produktbezeichnung` (NO!) +- **Reason:** User comments may be predefined terms referenced by external systems + +### Apply To ALL Files + +**For EVERY Module property** (3 files): +1. `.model.ts` → Property `@UnifiedField({ description: '...' })` +2. `inputs/-create.input.ts` → Property `@UnifiedField({ description: '...' })` +3. `inputs/.input.ts` → Property `@UnifiedField({ description: '...' })` + +**For EVERY SubObject property** (3 files): +1. `objects//.object.ts` → Property `@UnifiedField({ description: '...' })` +2. `objects//-create.input.ts` → Property `@UnifiedField({ description: '...' })` +3. `objects//.input.ts` → Property `@UnifiedField({ description: '...' })` + +**For class decorators**: +- `@ObjectType({ description: '...' })` on Models and Objects +- `@InputType({ description: '...' })` on all Input classes + +### Common Mistakes + +❌ **WRONG:** Descriptions only in Model, missing in Inputs +❌ **WRONG:** German-only descriptions without English translation +❌ **WRONG:** Inconsistent descriptions (different in Model vs Input) +❌ **WRONG:** Ignoring user-provided comments from specification +❌ **WRONG:** Changing wording: `Straße` → `Straßenname` (rephrased!) +❌ **WRONG:** Expanding terms: `Produkt` → `Produktbezeichnung` (added word!) + +✅ **CORRECT:** Same description in ALL 3 files (Model, CreateInput, UpdateInput) +✅ **CORRECT:** Format `ENGLISH (DEUTSCH)` for German comments +✅ **CORRECT:** All user comments extracted and applied +✅ **CORRECT:** Fix typos only, preserve original wording: `Postleizahl` → `Postleitzahl` +✅ **CORRECT:** Keep exact terms: `Straße` → `Street (Straße)` (not "Street name"!) ## Inheritance Handling @@ -326,7 +369,18 @@ Final checks before completing: ☐ All Objects created ☐ All Modules created ☐ Properties in alphabetical order -☐ Descriptions: "ENGLISH (DEUTSCH)" format +☐ DESCRIPTIONS - CRITICAL (check ALL): + ☐ User comments extracted from specification + ☐ German descriptions → ENGLISH (DEUTSCH) format + ☐ English descriptions → kept as-is + ☐ Module Models have descriptions + ☐ Module CreateInputs have SAME descriptions + ☐ Module UpdateInputs have SAME descriptions + ☐ SubObjects have descriptions + ☐ SubObject CreateInputs have SAME descriptions + ☐ SubObject UpdateInputs have SAME descriptions + ☐ @ObjectType/@InputType decorators have descriptions + ☐ NO inconsistencies between files ☐ Inheritance correctly implemented ☐ CreateInputs have all required fields (parent + model) ☐ Enum files created in src/server/common/enums/ diff --git a/src/templates/claude-skills/nest-server-generator/security-rules.md b/src/templates/claude-skills/nest-server-generator/security-rules.md new file mode 100644 index 0000000..8beffe2 --- /dev/null +++ b/src/templates/claude-skills/nest-server-generator/security-rules.md @@ -0,0 +1,358 @@ +--- +name: nest-server-generator-security-rules +version: 1.0.0 +description: Critical security and test coverage rules for NestJS development +--- + +# 🚨 CRITICAL SECURITY RULES + +**Before you start ANY work, understand these NON-NEGOTIABLE rules.** + +--- + +## ⛔ NEVER Do This + +1. **NEVER remove or weaken `@Restricted()` decorators** to make tests pass +2. **NEVER change `@Roles()` decorators** to more permissive roles for test convenience +3. **NEVER modify `securityCheck()` logic** to bypass security in tests +4. **NEVER remove class-level `@Restricted(RoleEnum.ADMIN)`** - it's a security fallback + +--- + +## ✅ ALWAYS Do This + +1. **ALWAYS analyze permissions BEFORE writing tests** (Controller, Model, Service layers) +2. **ALWAYS test with the LEAST privileged user** who is authorized +3. **ALWAYS create appropriate test users** for each permission level +4. **ALWAYS adapt tests to security requirements**, never the other way around +5. **ALWAYS ask developer for approval** before changing ANY security decorator +6. **ALWAYS aim for maximum test coverage** (80-100% depending on criticality) + +--- + +## 🔑 Permission Hierarchy (Specific Overrides General) + +```typescript +@Restricted(RoleEnum.ADMIN) // ← FALLBACK: DO NOT REMOVE +export class ProductController { + @Roles(RoleEnum.S_USER) // ← SPECIFIC: This method is more open + async createProduct() { } // ← S_USER can access (specific wins) + + async secretMethod() { } // ← ADMIN only (fallback applies) +} +``` + +**Why class-level `@Restricted(ADMIN)` MUST stay:** +- If someone forgets `@Roles()` on a new method → it's secure by default +- Shows the class is security-sensitive +- Fail-safe protection + +--- + +## Rule 1: NEVER Weaken Security for Test Convenience + +### ❌ ABSOLUTELY FORBIDDEN + +```typescript +// BEFORE (secure): +@Restricted(RoleEnum.ADMIN) +export class ProductController { + @Roles(RoleEnum.S_USER) + async createProduct() { ... } +} + +// AFTER (FORBIDDEN - security weakened!): +// @Restricted(RoleEnum.ADMIN) ← NEVER remove this! +export class ProductController { + @Roles(RoleEnum.S_USER) + async createProduct() { ... } +} +``` + +### 🚨 CRITICAL RULE + +- **NEVER remove or weaken `@Restricted()` decorators** on Controllers, Resolvers, Models, or Objects +- **NEVER change `@Roles()` decorators** to more permissive roles just to make tests pass +- **NEVER modify `securityCheck()` logic** to bypass security for testing + +### If tests fail due to permissions + +1. ✅ **CORRECT**: Adjust the test to use the appropriate user/token +2. ✅ **CORRECT**: Create test users with the required roles +3. ❌ **WRONG**: Weaken security to make tests pass + +### Any security changes MUST + +- Be discussed with the developer FIRST +- Have a solid business justification +- Be explicitly approved by the developer +- Be documented with the reason + +--- + +## Rule 2: Understanding Permission Hierarchy + +### ⭐ Key Concept: Specific Overrides General + +The `@Restricted()` decorator on a class acts as a **security fallback** - if a method/property doesn't specify permissions, it inherits the class-level restriction. This is a **security-by-default** pattern. + +### Example - Controller/Resolver + +```typescript +@Restricted(RoleEnum.ADMIN) // ← FALLBACK: Protects everything by default +export class ProductController { + + @Roles(RoleEnum.S_EVERYONE) // ← SPECIFIC: This method is MORE open + async getPublicProducts() { + // Anyone can access this (specific @Roles wins) + } + + @Roles(RoleEnum.S_USER) // ← SPECIFIC: Logged-in users + async createProduct() { + // S_USER can access (specific wins over fallback) + } + + async deleteProduct() { + // ADMIN ONLY (no specific decorator, fallback applies) + } +} +``` + +### Example - Model + +```typescript +@Restricted(RoleEnum.ADMIN) // ← FALLBACK +export class Product { + + @Roles(RoleEnum.S_EVERYONE) // ← SPECIFIC + @UnifiedField({ description: 'Product name' }) + name: string; // Everyone can read this + + @UnifiedField({ description: 'Internal cost' }) + cost: number; // ADMIN ONLY (fallback applies) +} +``` + +--- + +## Rule 3: Adapt Tests to Security, Not Vice Versa + +### ❌ WRONG Approach + +```typescript +// Test fails because user isn't admin +it('should create product', async () => { + const result = await request(app) + .post('/products') + .set('Authorization', regularUserToken) // Not an admin! + .send(productData); + + expect(result.status).toBe(201); // Fails with 403 +}); + +// ❌ WRONG FIX: Removing @Restricted from controller +// @Restricted(RoleEnum.ADMIN) ← NEVER DO THIS! +``` + +### ✅ CORRECT Approach + +```typescript +// Analyze first: Who is allowed to create products? +// Answer: ADMIN only (based on @Restricted on controller) + +// Create admin test user +let adminToken: string; + +beforeAll(async () => { + const admin = await createTestUser({ roles: [RoleEnum.ADMIN] }); + adminToken = admin.token; +}); + +it('should create product as admin', async () => { + const result = await request(app) + .post('/products') + .set('Authorization', adminToken) // ✅ Use admin token + .send(productData); + + expect(result.status).toBe(201); // ✅ Passes +}); + +it('should reject product creation for regular user', async () => { + const result = await request(app) + .post('/products') + .set('Authorization', regularUserToken) + .send(productData); + + expect(result.status).toBe(403); // ✅ Test security works! +}); +``` + +--- + +## Rule 4: Test with Least Privileged User + +**Always test with the LEAST privileged user who is authorized to perform the action.** + +### ❌ WRONG + +```typescript +// Method allows S_USER, but testing with ADMIN +@Roles(RoleEnum.S_USER) +async getProducts() { } + +it('should get products', async () => { + const result = await request(app) + .get('/products') + .set('Authorization', adminToken); // ❌ Over-privileged! +}); +``` + +### ✅ CORRECT + +```typescript +@Roles(RoleEnum.S_USER) +async getProducts() { } + +it('should get products as regular user', async () => { + const result = await request(app) + .get('/products') + .set('Authorization', regularUserToken); // ✅ Least privilege +}); +``` + +**Why this matters:** +- Tests might pass with ADMIN but fail with S_USER +- You won't catch permission bugs +- False confidence in security + +--- + +## Rule 5: Create Appropriate Test Users + +**Create test users for EACH permission level you need to test.** + +### Example Test Setup + +```typescript +describe('ProductController', () => { + let adminToken: string; + let userToken: string; + let everyoneToken: string; + + beforeAll(async () => { + // Create admin user + const admin = await createTestUser({ + roles: [RoleEnum.ADMIN] + }); + adminToken = admin.token; + + // Create regular user + const user = await createTestUser({ + roles: [RoleEnum.S_USER] + }); + userToken = user.token; + + // Create unauthenticated scenario + const guest = await createTestUser({ + roles: [RoleEnum.S_EVERYONE] + }); + everyoneToken = guest.token; + }); + + it('admin can delete products', async () => { + // Use adminToken + }); + + it('regular user can create products', async () => { + // Use userToken + }); + + it('everyone can view products', async () => { + // Use everyoneToken or no token + }); + + it('regular user cannot delete products', async () => { + // Use userToken, expect 403 + }); +}); +``` + +--- + +## Rule 6: Comprehensive Test Coverage + +**Aim for 80-100% test coverage depending on criticality:** + +- **High criticality** (payments, user data, admin functions): 95-100% +- **Medium criticality** (business logic, CRUD): 80-90% +- **Low criticality** (utilities, formatters): 70-80% + +### What to Test + +**For each endpoint/method:** + +1. ✅ Happy path (authorized user, valid data) +2. ✅ Permission denied (unauthorized user) +3. ✅ Validation errors (invalid input) +4. ✅ Edge cases (empty data, boundaries) +5. ✅ Error handling (server errors, missing resources) + +### Example Comprehensive Tests + +```typescript +describe('createProduct', () => { + it('should create product with admin user', async () => { + // Happy path + }); + + it('should reject creation by regular user', async () => { + // Permission test + }); + + it('should reject invalid product data', async () => { + // Validation test + }); + + it('should reject duplicate product name', async () => { + // Business rule test + }); + + it('should handle missing required fields', async () => { + // Edge case + }); +}); +``` + +--- + +## Quick Security Checklist + +Before completing ANY task: + +- [ ] **All @Restricted decorators preserved** +- [ ] **@Roles decorators NOT made more permissive** +- [ ] **Tests use appropriate user roles** +- [ ] **Test users created for each permission level** +- [ ] **Least privileged user tested** +- [ ] **Permission denial tested (403 responses)** +- [ ] **No securityCheck() logic bypassed** +- [ ] **Test coverage ≥ 80%** +- [ ] **All edge cases covered** + +--- + +## Security Decision Protocol + +**When you encounter a security-related decision:** + +1. **STOP** - Don't make the change immediately +2. **ANALYZE** - Why does the current security exist? +3. **ASK** - Consult the developer before changing +4. **DOCUMENT** - If approved, document the reason +5. **TEST** - Ensure security still works after change + +**Remember:** +- **Security > Convenience** +- **Better to over-restrict than under-restrict** +- **Always preserve existing security mechanisms** +- **When in doubt, ask the developer** diff --git a/src/templates/claude-skills/story-tdd/SKILL.md b/src/templates/claude-skills/story-tdd/SKILL.md new file mode 100644 index 0000000..42c0786 --- /dev/null +++ b/src/templates/claude-skills/story-tdd/SKILL.md @@ -0,0 +1,1173 @@ +--- +name: story-tdd +version: 1.0.2 +description: Expert for Test-Driven Development (TDD) with NestJS and @lenne.tech/nest-server. Creates story tests in test/stories/, analyzes requirements, writes comprehensive tests, then uses nest-server-generator skill to implement features until all tests pass. Ensures high code quality and security compliance. Use in projects with @lenne.tech/nest-server in package.json dependencies (supports monorepos with projects/*, packages/*, apps/* structure). +--- + +# Story-Based Test-Driven Development Expert + +You are an expert in Test-Driven Development (TDD) for NestJS applications using @lenne.tech/nest-server. You help developers implement new features by first creating comprehensive story tests, then iteratively developing the code until all tests pass. + +## When to Use This Skill + +**✅ ALWAYS use this skill for:** +- Implementing new API features using Test-Driven Development +- Creating story tests for user stories or requirements +- Developing new functionality in a test-first approach +- Ensuring comprehensive test coverage for new features +- Iterative development with test validation + +**🔄 This skill works closely with:** +- `nest-server-generator` skill for code implementation (modules, objects, properties) +- Existing test suites for understanding patterns +- API documentation (Swagger/Controllers) for interface design + +## Core TDD Workflow - The Seven Steps + +This skill follows a rigorous 7-step iterative process (with Steps 5, 5a, 5b for final validation and refactoring): + +### Step 1: Story Analysis & Validation + +**Before writing ANY code or tests:** + +1. **Read and analyze the complete user story/requirement** + - Identify all functional requirements + - List all acceptance criteria + - Note any technical constraints + +2. **Understand existing API structure** + - Examine relevant Controllers (REST endpoints) + - Review Swagger documentation + - Check existing GraphQL resolvers if applicable + - Identify related modules and services + +3. **Identify contradictions or ambiguities** + - Look for conflicting requirements + - Check for unclear specifications + - Verify if requirements match existing architecture + +4. **Ask developer for clarification IMMEDIATELY if needed** + - Don't assume or guess requirements + - Clarify contradictions BEFORE writing tests + - Get confirmation on architectural decisions + - Verify security/permission requirements + +**⚠️ CRITICAL:** If you find ANY contradictions or ambiguities, STOP and use AskUserQuestion to clarify BEFORE proceeding to Step 2. + +### Step 2: Create Story Test + +**⚠️ CRITICAL: Test Type Requirement** + +**ONLY create API tests using TestHelper - NEVER create direct Service tests!** + +- ✅ **DO:** Create tests that call REST endpoints or GraphQL queries/mutations using `TestHelper` +- ✅ **DO:** Test through the API layer (Controller/Resolver → Service → Database) +- ❌ **DON'T:** Create tests that directly instantiate or call Service methods +- ❌ **DON'T:** Create unit tests for Services (e.g., `user.service.spec.ts`) +- ❌ **DON'T:** Mock dependencies or bypass the API layer + +**Why API tests only?** +- API tests validate the complete security model (decorators, guards, permissions) +- Direct Service tests bypass authentication and authorization checks +- TestHelper provides all necessary tools for comprehensive API testing + +**Exception: Direct database/service access for test setup/cleanup ONLY** + +Direct database or service access is ONLY allowed for: + +- ✅ **Test Setup (beforeAll/beforeEach)**: + - Setting user roles in database: `await db.collection('users').updateOne({ _id: userId }, { $set: { roles: ['admin'] } })` + - Setting verified flag: `await db.collection('users').updateOne({ _id: userId }, { $set: { verified: true } })` + - Creating prerequisite test data that can't be created via API + +- ✅ **Test Cleanup (afterAll/afterEach)**: + - Deleting test objects: `await db.collection('products').deleteMany({ createdBy: testUserId })` + - Cleaning up test data: `await db.collection('users').deleteOne({ email: 'test@example.com' })` + +- ❌ **NEVER for testing functionality**: + - Don't call `userService.create()` to test user creation - use API endpoint! + - Don't call `productService.update()` to test updates - use API endpoint! + - Don't access database to verify results - query via API instead! + +**Example of correct usage:** + +```typescript +describe('User Registration Story', () => { + let testHelper: TestHelper; + let db: Db; + let createdUserId: string; + + beforeAll(async () => { + testHelper = new TestHelper(app); + db = app.get(getConnectionToken()).db; + }); + + afterAll(async () => { + // ✅ ALLOWED: Direct DB access for cleanup + if (createdUserId) { + await db.collection('users').deleteOne({ _id: new ObjectId(createdUserId) }); + } + }); + + it('should allow new user to register with valid data', async () => { + // ✅ CORRECT: Test via API + const result = await testHelper.rest('/auth/signup', { + method: 'POST', + payload: { + email: 'newuser@test.com', + password: 'SecurePass123!', + firstName: 'John', + lastName: 'Doe' + }, + statusCode: 201 + }); + + expect(result.id).toBeDefined(); + expect(result.email).toBe('newuser@test.com'); + createdUserId = result.id; + + // ✅ ALLOWED: Set verified flag for subsequent tests + await db.collection('users').updateOne( + { _id: new ObjectId(createdUserId) }, + { $set: { verified: true } } + ); + }); + + it('should allow verified user to sign in', async () => { + // ✅ CORRECT: Test via API + const result = await testHelper.rest('/auth/signin', { + method: 'POST', + payload: { + email: 'newuser@test.com', + password: 'SecurePass123!' + }, + statusCode: 201 + }); + + expect(result.token).toBeDefined(); + expect(result.user.email).toBe('newuser@test.com'); + + // ❌ WRONG: Don't verify via direct DB access + // const dbUser = await db.collection('users').findOne({ email: 'newuser@test.com' }); + + // ✅ CORRECT: Verify via API + const profile = await testHelper.rest('/api/users/me', { + method: 'GET', + token: result.token, + statusCode: 200 + }); + expect(profile.email).toBe('newuser@test.com'); + }); +}); +``` + +--- + +**Location:** `test/stories/` directory (create if it doesn't exist) + +**Directory Creation:** +If the `test/stories/` directory doesn't exist yet, create it first: +```bash +mkdir -p test/stories +``` + +**Naming Convention:** `{feature-name}.story.test.ts` +- Example: `user-registration.story.test.ts` +- Example: `product-search.story.test.ts` +- Example: `order-processing.story.test.ts` + +**Test Structure:** + +1. **Study existing story tests** (if any exist in `test/stories/`) + - Follow established patterns and conventions + - Use similar setup/teardown approaches + - Match coding style and organization + +2. **Study other test files** for patterns: + - Check `test/**/*.test.ts` files + - Understand authentication setup + - Learn data creation patterns + - See how API calls are made + +3. **Write comprehensive story test** that includes: + - Clear test description matching the story + - Setup of test data and users + - All acceptance criteria as test cases + - Proper authentication/authorization + - Validation of responses and side effects + - Cleanup/teardown + +4. **Ensure tests cover:** + - Happy path scenarios + - Edge cases + - Error conditions + - Security/permission checks + - Data validation + +**Example test structure:** +```typescript +describe('User Registration Story', () => { + let createdUserIds: string[] = []; + let createdProductIds: string[] = []; + + // Setup + beforeAll(async () => { + // Initialize test environment + }); + + afterAll(async () => { + // 🧹 CLEANUP: Delete ALL test data created during tests + // This prevents side effects on subsequent test runs + if (createdUserIds.length > 0) { + await db.collection('users').deleteMany({ + _id: { $in: createdUserIds.map(id => new ObjectId(id)) } + }); + } + if (createdProductIds.length > 0) { + await db.collection('products').deleteMany({ + _id: { $in: createdProductIds.map(id => new ObjectId(id)) } + }); + } + }); + + it('should allow new user to register with valid data', async () => { + // Test implementation + const user = await createUser(...); + createdUserIds.push(user.id); // Track for cleanup + }); + + it('should reject registration with invalid email', async () => { + // Test implementation + }); + + it('should prevent duplicate email registration', async () => { + // Test implementation + }); +}); +``` + +**🚨 CRITICAL: Test Data Cleanup** + +**ALWAYS implement comprehensive cleanup in your story tests!** + +Test data that remains in the database can cause side effects in subsequent test runs, leading to: +- False positives/negatives in tests +- Flaky tests that pass/fail randomly +- Contaminated test database +- Hard-to-debug test failures + +**Cleanup Strategy:** + +1. **Track all created entities:** + ```typescript + let createdUserIds: string[] = []; + let createdProductIds: string[] = []; + let createdOrderIds: string[] = []; + ``` + +2. **Add IDs immediately after creation:** + ```typescript + const user = await testHelper.rest('/api/users', { + method: 'POST', + payload: userData, + token: adminToken, + }); + createdUserIds.push(user.id); // ✅ Track for cleanup + ``` + +3. **Delete ALL created entities in afterAll:** + ```typescript + afterAll(async () => { + // Clean up all test data + if (createdOrderIds.length > 0) { + await db.collection('orders').deleteMany({ + _id: { $in: createdOrderIds.map(id => new ObjectId(id)) } + }); + } + if (createdProductIds.length > 0) { + await db.collection('products').deleteMany({ + _id: { $in: createdProductIds.map(id => new ObjectId(id)) } + }); + } + if (createdUserIds.length > 0) { + await db.collection('users').deleteMany({ + _id: { $in: createdUserIds.map(id => new ObjectId(id)) } + }); + } + + await connection.close(); + await app.close(); + }); + ``` + +4. **Clean up in correct order:** + - Delete child entities first (e.g., Orders before Products) + - Delete parent entities last (e.g., Users last) + - Consider foreign key relationships + +5. **Handle cleanup errors gracefully:** + ```typescript + afterAll(async () => { + try { + // Cleanup operations + if (createdUserIds.length > 0) { + await db.collection('users').deleteMany({ + _id: { $in: createdUserIds.map(id => new ObjectId(id)) } + }); + } + } catch (error) { + console.error('Cleanup failed:', error); + // Don't throw - cleanup failures shouldn't fail the test suite + } + + await connection.close(); + await app.close(); + }); + ``` + +**What to clean up:** +- ✅ Users created during tests +- ✅ Products/Resources created during tests +- ✅ Orders/Transactions created during tests +- ✅ Any relationships (comments, reviews, etc.) +- ✅ Files uploaded during tests +- ✅ Any other test data that persists + +**What NOT to clean up:** +- ❌ Global test users created in `beforeAll` that are reused (clean these once at the end) +- ❌ Database connections (close these separately) +- ❌ The app instance (close this separately) + +### Step 3: Run Tests & Analyze Failures + +**Execute all tests:** +```bash +npm test +``` + +**Or run specific story test:** +```bash +npm test -- test/stories/your-story.story.test.ts +``` + +**Analyze results:** +1. Record which tests fail and why +2. Identify if failures are due to: + - Missing implementation (expected) + - Test errors/bugs (needs fixing) + - Misunderstood requirements (needs clarification) + +**Decision point:** +- If test has bugs/errors → Go to Step 3a +- If API implementation is missing/incomplete → Go to Step 4 + +**Debugging Test Failures:** + +If test failures are unclear, enable debugging tools: +- **TestHelper:** Add `log: true, logError: true` to test options for detailed output +- **Server logging:** Set `logExceptions: true` in `src/config.env.ts` +- **Validation debugging:** Set `DEBUG_VALIDATION=true` environment variable + +See **reference.md** for detailed debugging instructions and examples. + +### Step 3a: Fix Test Errors (if needed) + +**Only fix tests if:** +- Test logic is incorrect +- Test has programming errors +- Test makes nonsensical demands +- Test doesn't match actual requirements + +**Do NOT "fix" tests by:** +- Removing security checks to make them pass +- Lowering expectations to match incomplete implementation +- Skipping test cases that should work + +**After fixing tests:** +- Return to Step 3 (run tests again) + +### Step 4: Implement/Extend API Code + +**Use the `nest-server-generator` skill for implementation:** + +1. **Analyze what's needed:** + - New modules? → Use `nest-server-generator` + - New objects? → Use `nest-server-generator` + - New properties? → Use `nest-server-generator` + - Code modifications? → Use `nest-server-generator` + +2. **Understand existing codebase first:** + - Read relevant source files + - Study @lenne.tech/nest-server patterns (in `node_modules/@lenne.tech/nest-server/src`) + - Check CrudService base class for services (in `node_modules/@lenne.tech/nest-server/src/core/common/services/crud.service.ts`) + - Check RoleEnum (in the project or, if not available, in `node_modules/@lenne.tech/nest-server/src/core/common/enums/role.enum.ts), where all user types/user roles are listed and described in the comments. + - The decorators @Roles, @Restricted, and @UnifiedField, together with the checkSecurity method in the models, data preparation in MapAndValidatePipe (node_modules/@lenne.tech/nest-server/src/core/common/pipes/map-and-validate.pipe.ts), controllers, services, and other mechanisms, determine what is permitted and what is returned. + - Review existing similar implementations + +3. **Implement equivalently to existing code:** + - Use TestHelper for REST oder GraphQL requests (in `node_modules/@lenne.tech/nest-server/src/test/test.helper.ts`) + - Match coding style and patterns + - Use same architectural approaches + - Follow established conventions + - Reuse existing utilities + +4. **🔍 IMPORTANT: Database Indexes** + + **Always define indexes directly in the @UnifiedField decorator via mongoose option!** + + **Quick Guidelines:** + - Fields used in queries → Add `mongoose: { index: true, type: String }` + - Foreign keys → Add index + - Unique fields → Add `mongoose: { index: true, unique: true, type: String }` + - ⚠️ NEVER define indexes separately in schema files + + **📖 For detailed index patterns and examples, see: `database-indexes.md`** + +5. **Prefer existing packages:** + - Check if @lenne.tech/nest-server provides needed functionality + - Only add new npm packages as last resort + - If new package needed, verify: + - High quality and well-maintained + - Frequently used (npm downloads) + - Active maintenance + - Free license (preferably MIT) + - Long-term viability + +### Step 5: Validate & Iterate + +**Run ALL tests:** +```bash +npm test +``` + +**Check results:** + +✅ **All tests pass?** +- Continue to Step 5a (Code Quality Check) + +❌ **Some tests still fail?** +- Return to Step 3 (analyze failures) +- Continue iteration + +### Step 5a: Code Quality & Refactoring Check + +**BEFORE marking the task as complete, perform a code quality review!** + +Once all tests are passing, analyze your implementation for code quality issues: + +#### 1-3. Code Quality Review + +**Check for:** +- Code duplication (extract to private methods if used 2+ times) +- Common functionality (create helper functions) +- Similar code paths (consolidate with flexible parameters) +- Consistency with existing patterns + +**📖 For detailed refactoring patterns and examples, see: `code-quality.md`** + +#### 4. Review for Consistency + +**Ensure consistent patterns throughout your implementation:** +- Naming conventions match existing codebase +- Error handling follows project patterns +- Return types are consistent +- Similar operations use similar approaches + +#### 4a. Check Database Indexes + +**Verify that indexes are defined where needed:** + +**Quick check:** +- Fields used in find/filter → Has index? +- Foreign keys (userId, productId, etc.) → Has index? +- Unique fields (email, username) → Has unique: true? +- Fields used in sorting → Has index? + +**If indexes are missing:** +- Add to @UnifiedField decorator (mongoose option) +- Re-run tests +- Document query pattern + +**📖 For detailed verification checklist, see: `database-indexes.md`** + +#### 4b. Security Review + +**🔐 CRITICAL: Perform security review before final testing!** + +**ALWAYS review all code changes for security vulnerabilities.** + +**Quick Security Check:** +- [ ] @Restricted/@Roles decorators NOT removed or weakened +- [ ] Ownership checks in place (users can only access own data) +- [ ] All inputs validated with proper DTOs +- [ ] Sensitive fields marked with hideField: true +- [ ] No injection vulnerabilities +- [ ] Error messages don't expose sensitive data +- [ ] Authorization tests pass + +**Red Flags (STOP if found):** +- 🚩 @Restricted decorator removed +- 🚩 @Roles changed to more permissive +- 🚩 Missing ownership checks +- 🚩 Sensitive fields exposed +- 🚩 'any' type instead of DTO + +**If ANY red flag found:** +1. STOP implementation +2. Fix security issue immediately +3. Re-run security checklist +4. Update tests to verify security + +**📖 For complete security checklist with examples, see: `security-review.md`** + +#### 5. Refactoring Decision Tree + +``` +Code duplication detected? + │ + ├─► Used in 2+ places? + │ │ + │ ├─► YES: Extract to private method + │ │ │ + │ │ └─► Used across multiple services? + │ │ │ + │ │ ├─► YES: Consider utility class/function + │ │ └─► NO: Keep as private method + │ │ + │ └─► NO: Leave as-is (don't over-engineer) + │ + └─► Complex logic block? + │ + ├─► Hard to understand? + │ └─► Extract to well-named method + │ + └─► Simple and clear? + └─► Leave as-is +``` + +#### 6. Run Tests After Refactoring & Security Review + +**CRITICAL: After any refactoring, adding indexes, or security fixes:** + +```bash +npm test +``` + +**Ensure:** +- ✅ All tests still pass +- ✅ No new failures introduced +- ✅ Code is more maintainable +- ✅ No functionality changed +- ✅ Indexes properly applied +- ✅ **Security checks still working (authorization tests pass)** + +#### 7. When to Skip Refactoring + +**Don't refactor if:** +- Code is used in only ONE place +- Extraction would make code harder to understand +- The duplication is coincidental, not conceptual +- Time constraints don't allow for safe refactoring + +**Remember:** +- **Working code > Perfect code** +- **Refactor only if it improves maintainability** +- **Always run tests after refactoring** +- **Always add indexes where queries are performed** + +### Step 5b: Final Validation + +**After refactoring (or deciding not to refactor):** + +1. **Run ALL tests one final time:** + ```bash + npm test + ``` + +2. **Verify:** + - ✅ All tests pass + - ✅ Test coverage is adequate + - ✅ Code follows project patterns + - ✅ No obvious duplication + - ✅ Clean and maintainable + - ✅ **Security review completed** + - ✅ **No security vulnerabilities introduced** + - ✅ **Authorization tests pass** + +3. **Generate final report for developer** + +4. **YOU'RE DONE!** 🎉 + +## 🔄 Handling Existing Tests When Modifying Code + +**CRITICAL RULE:** When your code changes cause existing (non-story) tests to fail, you MUST analyze and handle this properly. + +### Analysis Decision Tree + +When existing tests fail after your changes: + +``` +Existing test fails + │ + ├─► Was this change intentional and breaking? + │ │ + │ ├─► YES: Change was deliberate and it's clear why tests break + │ │ └─► ✅ Update the existing tests to reflect new behavior + │ │ - Modify test expectations + │ │ - Update test data/setup if needed + │ │ - Document why test was changed + │ │ + │ └─► NO/UNCLEAR: Not sure why tests are breaking + │ └─► 🔍 Investigate potential side effect + │ │ + │ ├─► Use git to review previous state: + │ │ - git show HEAD:path/to/file.ts + │ │ - git diff HEAD path/to/test.ts + │ │ - git log -p path/to/file.ts + │ │ + │ ├─► Compare old vs new behavior + │ │ + │ └─► ⚠️ Likely unintended side effect! + │ └─► Fix code to satisfy BOTH old AND new tests + │ - Refine implementation + │ - Add conditional logic if needed + │ - Ensure backward compatibility + │ - Keep existing functionality intact +``` + +### Using Git for Analysis (ALLOWED) + +**✅ Git commands are EXPLICITLY ALLOWED for analysis:** + +```bash +# View old version of a file +git show HEAD:src/server/modules/user/user.service.ts + +# See what changed in a file +git diff HEAD src/server/modules/user/user.service.ts + +# View file from specific commit +git show abc123:path/to/file.ts + +# See commit history for a file +git log -p --follow path/to/file.ts + +# Compare branches +git diff main..HEAD path/to/file.ts +``` + +**These commands help you understand:** +- What the code looked like before your changes +- What the previous test expectations were +- Why existing tests were written a certain way +- Whether your change introduces regression + +### Examples + +#### Example 1: Intentional Breaking Change + +```typescript +// Scenario: You added a required field to User model +// Old test expects: { email, firstName } +// New behavior requires: { email, firstName, lastName } + +// ✅ CORRECT: Update the test +it('should create user', async () => { + const user = await userService.create({ + email: 'test@example.com', + firstName: 'John', + lastName: 'Doe', // ✅ Added required field + }); + // ... +}); +``` + +#### Example 2: Unintended Side Effect + +```typescript +// Scenario: You changed authentication logic for new feature +// Old tests for different feature now fail unexpectedly + +// ❌ WRONG: Just update the failing tests +// ✅ CORRECT: Investigate and fix the code + +// 1. Use git to see old implementation +// git show HEAD:src/server/modules/auth/auth.service.ts + +// 2. Identify the unintended side effect +// 3. Refine your code to avoid breaking existing functionality + +// Example fix: Add conditional logic +async authenticate(user: User, options?: AuthOptions) { + // Your new feature logic + if (options?.useNewBehavior) { + return this.newAuthMethod(user); + } + + // Preserve existing behavior for backward compatibility + return this.existingAuthMethod(user); +} +``` + +### Guidelines + +**✅ DO update existing tests when:** +- You intentionally changed an API contract +- You removed deprecated functionality +- You renamed fields/methods +- The old behavior is being replaced (not extended) +- It's documented in your story requirements + +**❌ DON'T update existing tests when:** +- You're not sure why they're failing +- The failure seems unrelated to your story +- Multiple unrelated tests are breaking +- The test was testing important existing functionality + +**🔍 INVESTIGATE when:** +- More than 2-3 existing tests fail +- Tests in unrelated modules fail +- Test failure messages are unclear +- You suspect a side effect + +### Process + +1. **Run ALL tests** (not just story tests) + ```bash + npm test + ``` + +2. **If existing tests fail:** + ```bash + # Identify which tests failed + # For each failing test, decide: + ``` + +3. **For intentional changes:** + - Update test expectations + - Document change in commit message (when developer commits) + - Verify all tests pass + +4. **For unclear failures:** + - Use `git show` to see old code + - Use `git diff` to see your changes + - Compare old vs new behavior + - Refine code to fix both old AND new tests + +5. **Validate:** + ```bash + # All tests (old + new) should pass + npm test + ``` + +### Red Flags + +🚩 **Warning signs of unintended side effects:** +- Tests in different modules failing +- Security/auth tests failing +- Tests that worked in `main` branch now fail +- Tests with names unrelated to your story failing + +**When you see red flags:** +1. STOP updating tests +2. Use git to investigate +3. Fix the code, not the tests +4. Ask developer if uncertain + +### Remember + +- **Existing tests are documentation** of expected behavior +- **Don't break working functionality** to make new tests pass +- **Use git freely** for investigation (NOT for commits) +- **When in doubt, preserve backward compatibility** + +--- + +## ⛔ CRITICAL: GIT COMMITS + +**🚨 NEVER create git commits unless explicitly requested by the developer.** + +This is a **NON-NEGOTIABLE RULE**: + +1. ❌ **DO NOT** create git commits automatically after implementing features +2. ❌ **DO NOT** commit changes when tests pass +3. ❌ **DO NOT** assume the developer wants changes committed +4. ❌ **DO NOT** use git commands like `git add`, `git commit`, or `git push` unless explicitly asked + +**✅ ONLY create git commits when:** +- The developer explicitly asks: "commit these changes" +- The developer explicitly asks: "create a commit" +- The developer explicitly asks: "commit this to git" + +**Why this is important:** +- Developers may want to review changes before committing +- Developers may want to commit in specific chunks +- Developers may have custom commit workflows +- Automatic commits can disrupt developer workflows + +**Your responsibility:** +- ✅ Create and modify files as needed +- ✅ Run tests and ensure they pass +- ✅ Provide a comprehensive report of changes +- ❌ **NEVER commit to git without explicit request** + +**In your final report, you may remind the developer:** +```markdown +## Next Steps +The implementation is complete and all tests are passing. +You may want to review and commit these changes when ready. +``` + +**But NEVER execute git commands yourself unless explicitly requested.** + +--- + +## 🚨 CRITICAL SECURITY RULES + +### ⛔ NEVER Do This Without Explicit Approval: + +1. **NEVER remove or weaken `@Restricted()` decorators** +2. **NEVER change `@Roles() or @UnifiedField({roles})` to more permissive roles** +3. **NEVER modify `securityCheck()` logic** to bypass security +4. **NEVER remove class-level security decorators** +5. **NEVER disable authentication for convenience** + +### ✅ ALWAYS Do This: + +1. **ALWAYS analyze existing security mechanisms** before writing tests +2. **ALWAYS create appropriate test users** with correct roles +3. **ALWAYS test with least-privileged users** who should have access +4. **ALWAYS ask developer before changing ANY security decorator** +5. **ALWAYS preserve existing security architecture** + +### 🔑 When Tests Fail Due to Security: + +**CORRECT approach:** +```typescript +// Create test user (every logged-in user has the Role.S_USER role) +const res = await testHelper.rest('/auth/signin', { + method: 'POST', + payload: { + email: gUserEmail, + password: gUserPassword, + }, + statusCode: 201, +}); +gUserToken = res.token; + +// Verify user +await db.collection('users').updateOne({ _id: new ObjectId(res.id) }, { $set: { verified: true } }); + +// Or optionally specify additional roles (e.g., admin, if really necessary) +await db.collection('users').findOneAndUpdate({ _id: new ObjectId(res.id) }, { $set: { roles: ['admin'], verified: true } }); + +// Test with authenticated user via token +const result = testHelper.rest('/api/products', { + method: 'POST', + payload: input, + statusCode: 201, + token: gUserToken, +}); +``` + +**WRONG approach (NEVER do this):** +```typescript +// ❌ DON'T remove @Restricted decorator from controller +// ❌ DON'T change @Roles(ADMIN) to @Roles(S_USER) +// ❌ DON'T disable authentication +``` + +## Code Quality Standards + +### Must Follow Existing Patterns: + +1. **File organization:** Match existing structure +2. **Naming conventions:** Follow established patterns +3. **Import statements:** Group and order like existing files +4. **Error handling:** Use same approach as existing code +5. **Validation:** Follow existing validation patterns +6. **Documentation:** Match existing comment style + +### Minimize Dependencies: + +1. **First choice:** Use @lenne.tech/nest-server capabilities +2. **Second choice:** Use existing project dependencies +3. **Last resort:** Add new packages (with justification) + +### Test Quality: + +1. **Coverage:** Aim for 80-100% depending on criticality +2. **Clarity:** Tests should be self-documenting +3. **Independence:** Tests should not depend on each other +4. **Repeatability:** Tests should produce consistent results +5. **Speed:** Tests should run reasonably fast + +### 🚨 CRITICAL: NEVER USE `declare` KEYWORD FOR PROPERTIES + +**⚠️ IMPORTANT RULE: DO NOT use the `declare` keyword when defining properties in classes!** + +The `declare` keyword in TypeScript signals that a property is only a type declaration without a runtime value. This prevents decorators from being properly applied and overridden. + +**❌ WRONG - Using `declare`:** + +```typescript +export class ProductCreateInput extends ProductInput { + declare name: string; // ❌ WRONG - Decorator won't be applied! + declare price: number; // ❌ WRONG - Decorator won't be applied! +} +``` + +**✅ CORRECT - Without `declare`:** + +```typescript +export class ProductCreateInput extends ProductInput { + @UnifiedField({ description: 'Product name' }) + name: string; // ✅ CORRECT - Decorator works properly + + @UnifiedField({ description: 'Product price' }) + price: number; // ✅ CORRECT - Decorator works properly +} +``` + +**Why this matters:** + +1. **Decorators require actual properties**: `@UnifiedField()`, `@Restricted()`, and other decorators need actual property declarations to attach metadata +2. **Override behavior**: When extending classes, using `declare` prevents decorators from being properly overridden +3. **Runtime behavior**: `declare` properties don't exist at runtime, breaking the decorator system + +**Correct approach:** + +Use the `override` keyword (when appropriate) but NEVER `declare`: + +```typescript +export class ProductCreateInput extends ProductInput { + // ✅ Use override when useDefineForClassFields is enabled + override name: string; + + // ✅ Apply decorators directly - they will override parent decorators + @UnifiedField({ description: 'Product name', isOptional: false }) + override price: number; +} +``` + +**Remember: `declare` = no decorators = broken functionality!** + +## Autonomous Execution + +**You should work autonomously as much as possible:** + +1. ✅ Create test files without asking +2. ✅ Run tests without asking +3. ✅ Analyze failures and fix code without asking +4. ✅ Iterate through Steps 3-5 automatically +5. ✅ Use nest-server-generator skill as needed + +**Only ask developer when:** + +1. ❓ Story has contradictions/ambiguities (Step 1) +2. ❓ Security decorators need to be changed +3. ❓ New npm package needs to be added +4. ❓ Architectural decision with multiple valid approaches +5. ❓ Test keeps failing and you're unsure why + +## Final Report + +When all tests pass, provide a comprehensive report: + +### Report Structure: + +```markdown +# Story Implementation Complete ✅ + +## Story: [Story Name] + +### Tests Created +- Location: test/stories/[filename].story.test.ts +- Test cases: [number] scenarios +- Coverage: [coverage percentage if available] + +### Implementation Summary +- Modules created/modified: [list] +- Objects created/modified: [list] +- Properties added: [list] +- Other changes: [list] + +### Test Results +✅ All [number] tests passing +- [Brief summary of test scenarios] + +### Code Quality +- Followed existing patterns: ✅ +- Security preserved: ✅ +- No new dependencies added: ✅ (or list new dependencies with justification) +- Code duplication checked: ✅ +- Refactoring performed: [Yes/No - describe if yes] +- Database indexes added: ✅ + +### Security Review +- Authentication/Authorization: ✅ All decorators intact +- Input validation: ✅ All inputs validated +- Data exposure: ✅ Sensitive fields hidden +- Ownership checks: ✅ Proper authorization in services +- Injection prevention: ✅ No SQL/NoSQL injection risks +- Error handling: ✅ No data leakage in errors +- Security tests: ✅ All authorization tests pass + +### Refactoring (if performed) +- Extracted helper functions: [list with brief description] +- Consolidated code paths: [describe] +- Removed duplication: [describe] +- Tests still passing after refactoring: ✅ + +### Files Modified +1. [file path] - [what changed] +2. [file path] - [what changed] +... + +### Next Steps (if any) +- [Any recommendations or follow-up items] +``` + +## Common Patterns + +### Creating Test Users: + +```typescript +// Study existing tests to see the exact pattern used +// Common pattern example: + +// Create test user (every logged-in user has the Role.S_USER role) +const resUser = await testHelper.rest('/auth/signin', { + method: 'POST', + payload: { + email: gUserEmail, + password: gUserPassword, + }, + statusCode: 201, +}); +gUserToken = resUser.token; +await db.collection('users').updateOne({ _id: new ObjectId(resUser.id) }, { $set: { verified: true } }); + + +// Create admin user +const resAdmin = await testHelper.rest('/auth/signin', { + method: 'POST', + payload: { + email: gAdminEmail, + password: gAdminPassword, + }, + statusCode: 201, +}); +gAdminToken = resAdmin.token; +await db.collection('users').updateOne({ _id: new ObjectId(resAdmin.id) }, { $set: { roles: ['admin'], verified: true } }); +``` + +### Making Authenticated Requests: + +```typescript +// Study existing tests for the exact pattern +// Common REST API pattern: +const response = await testHelper.rest('/api/products', { + method: 'POST', + payload: input, + statusCode: 201, + token: gUserToken, +}); + +// Common GraphQL pattern: +const result = await testHelper.graphQl( + { + arguments: { + field: value, + }, + fields: ['id', 'name', { user: ['id', 'email'] }], + name: 'findProducts', + type: TestGraphQLType.QUERY, + }, + { token: gUserToken }, +); +``` + +### Test Organization: + +```typescript +describe('Feature Story', () => { + // Shared setup + let app: INestApplication; + let adminUser: User; + let normalUser: User; + + beforeAll(async () => { + // Initialize app, database, users + }); + + afterAll(async () => { + // Cleanup + }); + + describe('Happy Path', () => { + it('should work for authorized user', async () => { + // Test + }); + }); + + describe('Error Cases', () => { + it('should reject unauthorized access', async () => { + // Test + }); + + it('should validate input data', async () => { + // Test + }); + }); + + describe('Edge Cases', () => { + it('should handle special scenarios', async () => { + // Test + }); + }); +}); +``` + +## Integration with nest-server-generator + +**When to invoke nest-server-generator skill:** + +During Step 4 (Implementation), you should use the `nest-server-generator` skill for: + +1. **Module creation:** + ```bash + lt server module ModuleName --no-interactive [options] + ``` + +2. **Object creation:** + ```bash + lt server object ObjectName [options] + ``` + +3. **Adding properties:** + ```bash + lt server addProp ModuleName propertyName:type [options] + ``` + +4. **Understanding existing code:** + - Reading and analyzing Services (especially CrudService inheritance) + - Understanding Controllers and Resolvers + - Reviewing Models and DTOs + +**Best Practice:** Invoke the skill explicitly when you need to create or modify NestJS components, rather than editing files manually. + +## Remember + +1. **Tests first, code second** - Always write tests before implementation +2. **Iterate until green** - Don't stop until all tests pass +3. **Security review mandatory** - ALWAYS perform security check before final tests +4. **Refactor before done** - Check for duplication and extract common functionality +5. **Security is sacred** - Never compromise security for passing tests +6. **Quality over speed** - Take time to write good tests and clean code +7. **Ask when uncertain** - Clarify early to avoid wasted effort +8. **Autonomous execution** - Work independently, report comprehensively +9. **Equivalent implementation** - Match existing patterns and style +10. **Clean up test data** - Always implement comprehensive cleanup in afterAll + +Your goal is to deliver fully tested, high-quality, maintainable, **and secure** features that integrate seamlessly with the existing codebase while maintaining all security standards. \ No newline at end of file diff --git a/src/templates/claude-skills/story-tdd/code-quality.md b/src/templates/claude-skills/story-tdd/code-quality.md new file mode 100644 index 0000000..85d361a --- /dev/null +++ b/src/templates/claude-skills/story-tdd/code-quality.md @@ -0,0 +1,266 @@ +--- +name: story-tdd-code-quality +version: 1.0.0 +description: Code quality and refactoring guidelines for Test-Driven Development +--- + +# Code Quality & Refactoring Check + +**BEFORE marking the task as complete, perform a code quality review!** + +Once all tests are passing, analyze your implementation for code quality issues. + +--- + +## 1. Check for Code Duplication + +**Identify redundant code patterns:** +- Repeated logic in multiple methods +- Similar code blocks with minor variations +- Duplicated validation logic +- Repeated data transformations +- Multiple similar helper functions + +**Example of code duplication:** + +```typescript +// ❌ BAD: Duplicated validation logic +async createProduct(input: ProductInput) { + if (!input.name || input.name.trim().length === 0) { + throw new BadRequestException('Name is required'); + } + if (!input.price || input.price <= 0) { + throw new BadRequestException('Price must be positive'); + } + // ... create product +} + +async updateProduct(id: string, input: ProductInput) { + if (!input.name || input.name.trim().length === 0) { + throw new BadRequestException('Name is required'); + } + if (!input.price || input.price <= 0) { + throw new BadRequestException('Price must be positive'); + } + // ... update product +} + +// ✅ GOOD: Extracted to reusable function +private validateProductInput(input: ProductInput) { + if (!input.name || input.name.trim().length === 0) { + throw new BadRequestException('Name is required'); + } + if (!input.price || input.price <= 0) { + throw new BadRequestException('Price must be positive'); + } +} + +async createProduct(input: ProductInput) { + this.validateProductInput(input); + // ... create product +} + +async updateProduct(id: string, input: ProductInput) { + this.validateProductInput(input); + // ... update product +} +``` + +--- + +## 2. Extract Common Functionality + +**Look for opportunities to create helper functions:** +- Data transformation logic +- Validation logic +- Query building +- Response formatting +- Common calculations + +**Example of extracting common functionality:** + +```typescript +// ❌ BAD: Repeated price calculation logic +async createOrder(input: OrderInput) { + let totalPrice = 0; + for (const item of input.items) { + const product = await this.productService.findById(item.productId); + totalPrice += product.price * item.quantity; + } + // ... create order +} + +async estimateOrderPrice(items: OrderItem[]) { + let totalPrice = 0; + for (const item of items) { + const product = await this.productService.findById(item.productId); + totalPrice += product.price * item.quantity; + } + return totalPrice; +} + +// ✅ GOOD: Extracted to reusable helper +private async calculateOrderTotal(items: OrderItem[]): Promise { + let totalPrice = 0; + for (const item of items) { + const product = await this.productService.findById(item.productId); + totalPrice += product.price * item.quantity; + } + return totalPrice; +} + +async createOrder(input: OrderInput) { + const totalPrice = await this.calculateOrderTotal(input.items); + // ... create order +} + +async estimateOrderPrice(items: OrderItem[]) { + return this.calculateOrderTotal(items); +} +``` + +--- + +## 3. Consolidate Similar Code Paths + +**Identify code paths that can be unified:** +- Methods with similar logic but different parameters +- Conditional branches that can be combined +- Similar error handling patterns + +**Example of consolidating code paths:** + +```typescript +// ❌ BAD: Similar methods with duplicated logic +async findProductsByCategory(category: string) { + return this.find({ + where: { category }, + relations: ['reviews', 'supplier'], + order: { createdAt: 'DESC' }, + }); +} + +async findProductsBySupplier(supplierId: string) { + return this.find({ + where: { supplierId }, + relations: ['reviews', 'supplier'], + order: { createdAt: 'DESC' }, + }); +} + +async findProductsByPriceRange(minPrice: number, maxPrice: number) { + return this.find({ + where: { price: Between(minPrice, maxPrice) }, + relations: ['reviews', 'supplier'], + order: { createdAt: 'DESC' }, + }); +} + +// ✅ GOOD: Unified method with flexible filtering +async findProducts(filters: { + category?: string; + supplierId?: string; + priceRange?: { min: number; max: number }; +}) { + const where: any = {}; + + if (filters.category) { + where.category = filters.category; + } + + if (filters.supplierId) { + where.supplierId = filters.supplierId; + } + + if (filters.priceRange) { + where.price = Between(filters.priceRange.min, filters.priceRange.max); + } + + return this.find({ + where, + relations: ['reviews', 'supplier'], + order: { createdAt: 'DESC' }, + }); +} +``` + +--- + +## 4. Review for Consistency + +**Ensure consistent patterns throughout your implementation:** +- Naming conventions match existing codebase +- Error handling follows project patterns +- Return types are consistent +- Similar operations use similar approaches + +--- + +## 5. Refactoring Decision Tree + +``` +Code duplication detected? + │ + ├─► Used in 2+ places? + │ │ + │ ├─► YES: Extract to private method + │ │ │ + │ │ └─► Used across multiple services? + │ │ │ + │ │ ├─► YES: Consider utility class/function + │ │ └─► NO: Keep as private method + │ │ + │ └─► NO: Leave as-is (don't over-engineer) + │ + └─► Complex logic block? + │ + ├─► Hard to understand? + │ └─► Extract to well-named method + │ + └─► Simple and clear? + └─► Leave as-is +``` + +--- + +## 6. Run Tests After Refactoring + +**CRITICAL: After any refactoring:** + +```bash +npm test +``` + +**Ensure:** +- ✅ All tests still pass +- ✅ No new failures introduced +- ✅ Code is more maintainable +- ✅ No functionality changed + +--- + +## 7. When to Skip Refactoring + +**Don't refactor if:** +- Code is used in only ONE place +- Extraction would make code harder to understand +- The duplication is coincidental, not conceptual +- Time constraints don't allow for safe refactoring + +**Remember:** +- **Working code > Perfect code** +- **Refactor only if it improves maintainability** +- **Always run tests after refactoring** + +--- + +## Quick Code Quality Checklist + +Before marking complete: + +- [ ] **No obvious code duplication** +- [ ] **Common functionality extracted to helpers** +- [ ] **Consistent patterns throughout** +- [ ] **Code follows existing patterns** +- [ ] **Proper error handling** +- [ ] **Tests still pass after refactoring** diff --git a/src/templates/claude-skills/story-tdd/database-indexes.md b/src/templates/claude-skills/story-tdd/database-indexes.md new file mode 100644 index 0000000..574bb85 --- /dev/null +++ b/src/templates/claude-skills/story-tdd/database-indexes.md @@ -0,0 +1,173 @@ +--- +name: story-tdd-database-indexes +version: 1.0.0 +description: Database index guidelines for @UnifiedField decorator - keep indexes visible with properties +--- + +# 🔍 Database Indexes with @UnifiedField + +**IMPORTANT: Always define indexes directly in the @UnifiedField decorator!** + +This keeps indexes visible right where properties are defined, making them easy to spot during code reviews. + +--- + +## When to Add Indexes + +- ✅ Fields used in queries (find, filter, search) +- ✅ Foreign keys (references to other collections) +- ✅ Fields used in sorting operations +- ✅ Unique constraints (email, username, etc.) +- ✅ Fields frequently accessed together (compound indexes) + +--- + +## Example Patterns + +### Single Field Index + +```typescript +@UnifiedField({ + description: 'User email address', + mongoose: { index: true, unique: true, type: String } // ✅ Simple index + unique constraint +}) +email: string; +``` + +### Compound Index + +```typescript +@UnifiedField({ + description: 'Product category', + mongoose: { index: true, type: String } // ✅ Part of compound index +}) +category: string; + +@UnifiedField({ + description: 'Product status', + mongoose: { index: true, type: String } // ✅ Part of compound index +}) +status: string; + +// Both fields indexed individually for flexible querying +``` + +### Text Index for Search + +```typescript +@UnifiedField({ + description: 'Product name', + mongoose: { type: String, text: true } // ✅ Full-text search index +}) +name: string; +``` + +### Foreign Key Index + +```typescript +@UnifiedField({ + description: 'Reference to user who created this', + mongoose: { index: true, type: String } // ✅ Index for JOIN operations +}) +createdBy: string; +``` + +--- + +## ⚠️ DON'T Create Indexes Separately! + +```typescript +// ❌ WRONG: Separate schema index definition +@Schema() +export class Product { + @UnifiedField({ + description: 'Category', + mongoose: { type: String } + }) + category: string; +} + +ProductSchema.index({ category: 1 }); // ❌ Index hidden away from property + +// ✅ CORRECT: Index in decorator mongoose option +@Schema() +export class Product { + @UnifiedField({ + description: 'Category', + mongoose: { index: true, type: String } // ✅ Immediately visible + }) + category: string; +} +``` + +--- + +## Benefits of Decorator-Based Indexes + +- ✅ Indexes visible when reviewing properties +- ✅ No need to search schema files +- ✅ Clear documentation of query patterns +- ✅ Easier to maintain and update +- ✅ Self-documenting code + +--- + +## Index Verification Checklist + +**Look for fields that should have indexes:** +- Fields used in find/filter operations +- Foreign keys (userId, productId, etc.) +- Fields used in sorting (createdAt, updatedAt, name) +- Unique fields (email, username, slug) + +**Example check:** + +```typescript +// Service has this query: +const orders = await this.orderService.find({ + where: { customerId: userId, status: 'pending' } +}); + +// ✅ Model should have indexes: +export class Order { + @UnifiedField({ + description: 'Customer reference', + mongoose: { index: true, type: String } // ✅ Used in queries + }) + customerId: string; + + @UnifiedField({ + description: 'Order status', + mongoose: { index: true, type: String } // ✅ Used in filtering + }) + status: string; +} +``` + +--- + +## Red Flags - Missing Indexes + +🚩 **Check for these issues:** +- Service queries a field but model has no index +- Foreign key fields without index +- Unique constraints not marked in decorator +- Fields used in sorting without index + +**If indexes are missing:** +1. Add them to the @UnifiedField decorator immediately +2. Re-run tests to ensure everything still works +3. Document why the index is needed (query pattern) + +--- + +## Quick Index Checklist + +Before marking complete: + +- [ ] **Fields used in find() queries have indexes** +- [ ] **Foreign keys (userId, productId, etc.) have indexes** +- [ ] **Unique fields (email, username) marked with unique: true** +- [ ] **Fields used in sorting have indexes** +- [ ] **All indexes in @UnifiedField decorator (NOT separate schema)** +- [ ] **Indexes match query patterns in services** diff --git a/src/templates/claude-skills/story-tdd/examples.md b/src/templates/claude-skills/story-tdd/examples.md new file mode 100644 index 0000000..a0101d4 --- /dev/null +++ b/src/templates/claude-skills/story-tdd/examples.md @@ -0,0 +1,1332 @@ +--- +name: story-tdd-examples +version: 1.0.0 +description: Complete examples for Test-Driven Development workflow with NestJS story tests +--- + +# Story-Based TDD Examples + +This document provides complete examples of the TDD workflow for different types of user stories. + +## Example 1: Simple CRUD Feature - Product Reviews + +### Story Requirement + +``` +As a user, I want to add reviews to products so that I can share my experience with other customers. + +Acceptance Criteria: +- Users can create a review with rating (1-5) and comment +- Rating is required, comment is optional +- Only authenticated users can create reviews +- Users can view all reviews for a product +- Each review shows author name and creation date +``` + +### Step 1: Story Analysis + +**Analysis notes:** +- New feature, likely needs new Review module +- Needs relationship between Review and Product +- Security: Only authenticated users (S_USER role minimum) +- No mention of update/delete, so only CREATE and READ operations + +**Questions to clarify:** +- Can users edit their reviews? (Assuming NO for this example) +- Can users review a product multiple times? (Assuming NO) +- What validation for rating? (Assuming 1-5 integer) + +### Step 2: Create Story Test + +**File:** `test/stories/product-review.story.test.ts` + +```typescript +import { + ConfigService, + HttpExceptionLogFilter, + TestGraphQLType, + TestHelper, +} from '@lenne.tech/nest-server'; +import { Test, TestingModule } from '@nestjs/testing'; +import { PubSub } from 'graphql-subscriptions'; +import { MongoClient, ObjectId } from 'mongodb'; + +import envConfig from '../../src/config.env'; +import { RoleEnum } from '../../src/server/common/enums/role.enum'; +import { ProductService } from '../../src/server/modules/product/product.service'; +import { ReviewService } from '../../src/server/modules/review/review.service'; +import { UserService } from '../../src/server/modules/user/user.service'; +import { imports, ServerModule } from '../../src/server/server.module'; + +describe('Product Review Story', () => { + // Test environment properties + let app; + let testHelper: TestHelper; + + // Database + let connection; + let db; + + // Services + let userService: UserService; + let productService: ProductService; + let reviewService: ReviewService; + + // Global test data + let gAdminToken: string; + let gAdminId: string; + let gUserToken: string; + let gUserId: string; + let gProductId: string; + + // Track created entities for cleanup + let createdReviewIds: string[] = []; + + beforeAll(async () => { + // Start server for testing + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [...imports, ServerModule], + providers: [ + UserService, + ProductService, + ReviewService, + { + provide: 'PUB_SUB', + useValue: new PubSub(), + }, + ], + }).compile(); + + app = moduleFixture.createNestApplication(); + app.useGlobalFilters(new HttpExceptionLogFilter()); + app.setBaseViewsDir(envConfig.templates.path); + app.setViewEngine(envConfig.templates.engine); + await app.init(); + + testHelper = new TestHelper(app); + userService = moduleFixture.get(UserService); + productService = moduleFixture.get(ProductService); + reviewService = moduleFixture.get(ReviewService); + + // Connection to database + connection = await MongoClient.connect(envConfig.mongoose.uri); + db = await connection.db(); + + // Create admin user + const adminPassword = Math.random().toString(36).substring(7); + const adminEmail = `admin-${adminPassword}@test.com`; + const adminSignUp = await testHelper.graphQl({ + arguments: { + input: { + email: adminEmail, + firstName: 'Admin', + password: adminPassword, + }, + }, + fields: ['token', { user: ['id', 'email', 'roles'] }], + name: 'signUp', + type: TestGraphQLType.MUTATION, + }); + gAdminId = adminSignUp.user.id; + gAdminToken = adminSignUp.token; + + // Set admin role + await userService.update(gAdminId, { roles: [RoleEnum.ADMIN] }, gAdminId); + + // Create normal user + const userPassword = Math.random().toString(36).substring(7); + const userEmail = `user-${userPassword}@test.com`; + const userSignUp = await testHelper.graphQl({ + arguments: { + input: { + email: userEmail, + firstName: 'Test', + password: userPassword, + }, + }, + fields: ['token', { user: ['id', 'email'] }], + name: 'signUp', + type: TestGraphQLType.MUTATION, + }); + gUserId = userSignUp.user.id; + gUserToken = userSignUp.token; + + // Create test product + const product = await testHelper.rest('/api/products', { + method: 'POST', + payload: { + name: 'Test Product', + price: 99.99, + }, + token: gAdminToken, + }); + gProductId = product.id; + }); + + afterAll(async () => { + // 🧹 CLEANUP: Delete all test data created during tests + try { + // Delete all created reviews + if (createdReviewIds.length > 0) { + await db.collection('reviews').deleteMany({ + _id: { $in: createdReviewIds.map(id => new ObjectId(id)) } + }); + } + + // Delete test product + if (gProductId) { + await db.collection('products').deleteOne({ _id: new ObjectId(gProductId) }); + } + + // Delete test users + if (gUserId) { + await db.collection('users').deleteOne({ _id: new ObjectId(gUserId) }); + } + if (gAdminId) { + await db.collection('users').deleteOne({ _id: new ObjectId(gAdminId) }); + } + } catch (error) { + console.error('Cleanup failed:', error); + } + + await connection.close(); + await app.close(); + }); + + describe('Creating Reviews', () => { + it('should allow authenticated user to create review with rating and comment', async () => { + const review = await testHelper.rest('/api/reviews', { + method: 'POST', + payload: { + productId: gProductId, + rating: 5, + comment: 'Excellent product!', + }, + token: gUserToken, + }); + + expect(review).toMatchObject({ + rating: 5, + comment: 'Excellent product!', + authorId: gUserId, + }); + expect(review.id).toBeDefined(); + expect(review.createdAt).toBeDefined(); + + // Track for cleanup + createdReviewIds.push(review.id); + }); + + it('should allow review with rating only (no comment)', async () => { + const review = await testHelper.rest('/api/reviews', { + method: 'POST', + payload: { + productId: gProductId, + rating: 4, + }, + token: gUserToken, + }); + + expect(review.rating).toBe(4); + expect(review.comment).toBeUndefined(); + + // Track for cleanup + createdReviewIds.push(review.id); + }); + + it('should reject review without rating', async () => { + await testHelper.rest('/api/reviews', { + method: 'POST', + payload: { + productId: gProductId, + comment: 'Missing rating', + }, + statusCode: 400, + token: gUserToken, + }); + }); + + it('should reject review with invalid rating', async () => { + await testHelper.rest('/api/reviews', { + method: 'POST', + payload: { + productId: gProductId, + rating: 6, // Invalid: must be 1-5 + }, + statusCode: 400, + token: gUserToken, + }); + }); + + it('should reject unauthenticated review creation', async () => { + await testHelper.rest('/api/reviews', { + method: 'POST', + payload: { + productId: gProductId, + rating: 5, + comment: 'Trying without auth', + }, + statusCode: 401, + }); + }); + }); + + describe('Viewing Reviews', () => { + let createdReviewId: string; + + beforeAll(async () => { + // Create a review for testing + const review = await testHelper.rest('/api/reviews', { + method: 'POST', + payload: { + productId: gProductId, + rating: 5, + comment: 'Great product', + }, + token: gUserToken, + }); + createdReviewId = review.id; + + // Track for cleanup + createdReviewIds.push(review.id); + }); + + it('should allow anyone to view product reviews', async () => { + const reviews = await testHelper.rest(`/api/products/${gProductId}/reviews`); + + expect(reviews).toBeInstanceOf(Array); + expect(reviews.length).toBeGreaterThan(0); + + const review = reviews.find(r => r.id === createdReviewId); + expect(review).toMatchObject({ + rating: 5, + comment: 'Great product', + }); + expect(review.author).toBeDefined(); + expect(review.createdAt).toBeDefined(); + }); + + it('should return empty array for product with no reviews', async () => { + // Create product without reviews + const newProduct = await testHelper.rest('/api/products', { + method: 'POST', + payload: { + name: 'New Product', + price: 49.99, + }, + token: gAdminToken, + }); + + const reviews = await testHelper.rest(`/api/products/${newProduct.id}/reviews`); + expect(reviews).toEqual([]); + }); + }); +}); +``` + +### Step 3-5: Implementation Iteration + +**First run - Expected failures:** +``` +❌ POST /api/reviews → 404 (endpoint doesn't exist) +❌ GET /api/products/:id/reviews → 404 (endpoint doesn't exist) +``` + +**Implementation (using nest-server-generator):** +```bash +# Create Review module +lt server module Review --no-interactive + +# Add properties +lt server addProp Review productId:string --no-interactive +lt server addProp Review authorId:string --no-interactive +lt server addProp Review rating:number --no-interactive +lt server addProp Review comment:string? --no-interactive +``` + +**Manual adjustments needed:** +- Add validation for rating (1-5 range) +- Add @Restricted decorator with appropriate roles +- Add GET endpoint to ProductController for reviews +- Add relationship between Product and Review + +**Final run - All tests pass:** +``` +✅ All tests passing (8 scenarios) +``` + +--- + +## Example 2: Complex Business Logic - Order Processing + +### Story Requirement + +``` +As a customer, I want to place an order with multiple products so that I can purchase items together. + +Acceptance Criteria: +- Order contains multiple products with quantities +- Order calculates total price automatically +- Order cannot be created with empty product list +- Order requires delivery address +- Order status is initially "pending" +- Products are checked for availability +- Insufficient stock prevents order creation +``` + +### Step 1: Story Analysis + +**Analysis notes:** +- Needs Order module with relationship to Product +- Needs OrderItem subobject for quantity tracking +- Business logic: stock validation +- Calculated field: total price +- Complex validation rules + +**Architecture decisions:** +- Use SubObject for OrderItem (embedded in Order) +- Total price should be calculated in service layer +- Stock check happens in service before saving + +### Step 2: Create Story Test + +**File:** `test/stories/order-processing.story.test.ts` + +```typescript +import { + ConfigService, + HttpExceptionLogFilter, + TestGraphQLType, + TestHelper, +} from '@lenne.tech/nest-server'; +import { Test, TestingModule } from '@nestjs/testing'; +import { PubSub } from 'graphql-subscriptions'; +import { MongoClient, ObjectId } from 'mongodb'; + +import envConfig from '../../src/config.env'; +import { RoleEnum } from '../../src/server/common/enums/role.enum'; +import { OrderService } from '../../src/server/modules/order/order.service'; +import { ProductService } from '../../src/server/modules/product/product.service'; +import { UserService } from '../../src/server/modules/user/user.service'; +import { imports, ServerModule } from '../../src/server/server.module'; + +describe('Order Processing Story', () => { + // Test environment properties + let app; + let testHelper: TestHelper; + + // Database + let connection; + let db; + + // Services + let userService: UserService; + let productService: ProductService; + let orderService: OrderService; + + // Global test data + let gAdminToken: string; + let gAdminId: string; + let gCustomerToken: string; + let gCustomerId: string; + let gProduct1Id: string; + let gProduct1Stock: number; + let gProduct2Id: string; + + // Track created entities for cleanup + let createdOrderIds: string[] = []; + let createdProductIds: string[] = []; + + beforeAll(async () => { + // Start server for testing + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [...imports, ServerModule], + providers: [ + UserService, + ProductService, + OrderService, + { + provide: 'PUB_SUB', + useValue: new PubSub(), + }, + ], + }).compile(); + + app = moduleFixture.createNestApplication(); + app.useGlobalFilters(new HttpExceptionLogFilter()); + app.setBaseViewsDir(envConfig.templates.path); + app.setViewEngine(envConfig.templates.engine); + await app.init(); + + testHelper = new TestHelper(app); + userService = moduleFixture.get(UserService); + productService = moduleFixture.get(ProductService); + orderService = moduleFixture.get(OrderService); + + // Connection to database + connection = await MongoClient.connect(envConfig.mongoose.uri); + db = await connection.db(); + + // Create admin user + const adminPassword = Math.random().toString(36).substring(7); + const adminEmail = `admin-${adminPassword}@test.com`; + const adminSignUp = await testHelper.graphQl({ + arguments: { + input: { + email: adminEmail, + firstName: 'Admin', + password: adminPassword, + }, + }, + fields: ['token', { user: ['id', 'email'] }], + name: 'signUp', + type: TestGraphQLType.MUTATION, + }); + gAdminId = adminSignUp.user.id; + gAdminToken = adminSignUp.token; + + // Set admin role + await userService.update(gAdminId, { roles: [RoleEnum.ADMIN] }, gAdminId); + + // Create customer user + const customerPassword = Math.random().toString(36).substring(7); + const customerEmail = `customer-${customerPassword}@test.com`; + const customerSignUp = await testHelper.graphQl({ + arguments: { + input: { + email: customerEmail, + firstName: 'Customer', + password: customerPassword, + }, + }, + fields: ['token', { user: ['id'] }], + name: 'signUp', + type: TestGraphQLType.MUTATION, + }); + gCustomerId = customerSignUp.user.id; + gCustomerToken = customerSignUp.token; + + // Create test products with stock + const product1 = await testHelper.rest('/api/products', { + method: 'POST', + payload: { + name: 'Product A', + price: 10.00, + stock: 100, + }, + token: gAdminToken, + }); + gProduct1Id = product1.id; + gProduct1Stock = product1.stock; + + const product2 = await testHelper.rest('/api/products', { + method: 'POST', + payload: { + name: 'Product B', + price: 25.50, + stock: 50, + }, + token: gAdminToken, + }); + gProduct2Id = product2.id; + + // Track products for cleanup + createdProductIds.push(gProduct1Id, gProduct2Id); + }); + + afterAll(async () => { + // 🧹 CLEANUP: Delete all test data created during tests + try { + // Delete all created orders first (child entities) + if (createdOrderIds.length > 0) { + await db.collection('orders').deleteMany({ + _id: { $in: createdOrderIds.map(id => new ObjectId(id)) } + }); + } + + // Delete all created products + if (createdProductIds.length > 0) { + await db.collection('products').deleteMany({ + _id: { $in: createdProductIds.map(id => new ObjectId(id)) } + }); + } + + // Delete test users + if (gCustomerId) { + await db.collection('users').deleteOne({ _id: new ObjectId(gCustomerId) }); + } + if (gAdminId) { + await db.collection('users').deleteOne({ _id: new ObjectId(gAdminId) }); + } + } catch (error) { + console.error('Cleanup failed:', error); + } + + await connection.close(); + await app.close(); + }); + + describe('Order Creation - Happy Path', () => { + it('should create order with multiple products and calculate total', async () => { + const orderData = { + items: [ + { productId: gProduct1Id, quantity: 2 }, + { productId: gProduct2Id, quantity: 1 }, + ], + deliveryAddress: { + street: '123 Main St', + city: 'Test City', + zipCode: '12345', + country: 'Germany', + }, + }; + + const order = await testHelper.rest('/api/orders', { + method: 'POST', + payload: orderData, + token: gCustomerToken, + }); + + expect(order).toMatchObject({ + status: 'pending', + customerId: gCustomerId, + totalPrice: 45.50, // (10.00 * 2) + (25.50 * 1) + deliveryAddress: orderData.deliveryAddress, + }); + + expect(order.items).toHaveLength(2); + expect(order.items[0]).toMatchObject({ + productId: gProduct1Id, + quantity: 2, + priceAtOrder: 10.00, + }); + + // Track for cleanup + createdOrderIds.push(order.id); + }); + + it('should create order with single product', async () => { + const orderData = { + items: [ + { productId: gProduct1Id, quantity: 1 }, + ], + deliveryAddress: { + street: '456 Oak Ave', + city: 'Sample Town', + zipCode: '54321', + country: 'Germany', + }, + }; + + const order = await testHelper.rest('/api/orders', { + method: 'POST', + payload: orderData, + token: gCustomerToken, + }); + + expect(order.totalPrice).toBe(10.00); + + // Track for cleanup + createdOrderIds.push(order.id); + }); + }); + + describe('Order Validation', () => { + it('should reject order with empty product list', async () => { + const orderData = { + items: [], + deliveryAddress: { + street: '123 Main St', + city: 'Test City', + zipCode: '12345', + country: 'Germany', + }, + }; + + await testHelper.rest('/api/orders', { + method: 'POST', + payload: orderData, + statusCode: 400, + token: gCustomerToken, + }); + }); + + it('should reject order without delivery address', async () => { + const orderData = { + items: [ + { productId: gProduct1Id, quantity: 1 }, + ], + }; + + await testHelper.rest('/api/orders', { + method: 'POST', + payload: orderData, + statusCode: 400, + token: gCustomerToken, + }); + }); + + it('should reject order with invalid product ID', async () => { + const orderData = { + items: [ + { productId: 'invalid-id', quantity: 1 }, + ], + deliveryAddress: { + street: '123 Main St', + city: 'Test City', + zipCode: '12345', + country: 'Germany', + }, + }; + + await testHelper.rest('/api/orders', { + method: 'POST', + payload: orderData, + statusCode: 404, + token: gCustomerToken, + }); + }); + }); + + describe('Stock Management', () => { + it('should reject order when product stock is insufficient', async () => { + // Create product with limited stock + const limitedProduct = await testHelper.rest('/api/products', { + method: 'POST', + payload: { + name: 'Limited Product', + price: 100.00, + stock: 5, + }, + token: gAdminToken, + }); + + // Track for cleanup + createdProductIds.push(limitedProduct.id); + + const orderData = { + items: [ + { productId: limitedProduct.id, quantity: 10 }, // More than available + ], + deliveryAddress: { + street: '123 Main St', + city: 'Test City', + zipCode: '12345', + country: 'Germany', + }, + }; + + const response = await testHelper.rest('/api/orders', { + method: 'POST', + payload: orderData, + statusCode: 400, + token: gCustomerToken, + }); + + expect(response.message).toContain('insufficient stock'); + }); + + it('should reduce product stock after successful order', async () => { + const initialStock = gProduct1Stock; + + const orderData = { + items: [ + { productId: gProduct1Id, quantity: 3 }, + ], + deliveryAddress: { + street: '123 Main St', + city: 'Test City', + zipCode: '12345', + country: 'Germany', + }, + }; + + const order = await testHelper.rest('/api/orders', { + method: 'POST', + payload: orderData, + token: gCustomerToken, + }); + + // Track for cleanup + createdOrderIds.push(order.id); + + // Check product stock was reduced + const updatedProduct = await testHelper.rest(`/api/products/${gProduct1Id}`, { + token: gAdminToken, + }); + + expect(updatedProduct.stock).toBe(initialStock - 3); + + // Update global stock for subsequent tests + gProduct1Stock = updatedProduct.stock; + }); + }); + + describe('Authorization', () => { + it('should reject unauthenticated order creation', async () => { + const orderData = { + items: [ + { productId: gProduct1Id, quantity: 1 }, + ], + deliveryAddress: { + street: '123 Main St', + city: 'Test City', + zipCode: '12345', + country: 'Germany', + }, + }; + + await testHelper.rest('/api/orders', { + method: 'POST', + payload: orderData, + statusCode: 401, + }); + }); + }); +}); +``` + +### Implementation Steps + +**SubObject creation:** +```typescript +// Create OrderItem SubObject manually +// File: src/server/modules/order/order-item.subobject.ts + +@SubObjectType() +export class OrderItem { + @UnifiedField({ + description: 'Reference to product', + mongoose: { index: true, type: String } // ✅ Index for queries by product + }) + productId: string; + + @UnifiedField({ + description: 'Quantity ordered', + mongoose: { type: Number } + }) + quantity: number; + + @UnifiedField({ + description: 'Price when order was placed', + mongoose: { type: Number } + }) + priceAtOrder: number; +} +``` + +**Model with indexes:** +```typescript +// File: src/server/modules/order/order.model.ts + +@Schema() +export class Order { + @UnifiedField({ + description: 'Customer who placed the order', + mongoose: { index: true, type: String } // ✅ Frequent queries by customer + }) + customerId: string; + + @UnifiedField({ + description: 'Order status', + mongoose: { index: true, type: String } // ✅ Filtering by status + }) + status: string; + + @UnifiedField({ + description: 'Order items', + mongoose: { type: [OrderItem] } + }) + items: OrderItem[]; + + @UnifiedField({ + description: 'Total price calculated from items', + mongoose: { type: Number } + }) + totalPrice: number; + + @UnifiedField({ + description: 'Delivery address', + mongoose: { type: Object } + }) + deliveryAddress: Address; +} +``` + +**Why these indexes?** +- `customerId`: Service queries orders by customer → needs index +- `status`: Service filters by status (pending, completed) → needs index +- Both indexed individually for flexible querying + +**Service logic for total calculation and stock validation:** +```typescript +// In OrderService (extends CrudService) + +async create(input: CreateOrderInput, userId: string): Promise { + // Validate items exist + if (!input.items || input.items.length === 0) { + throw new BadRequestException('Order must contain at least one item'); + } + + // Check stock and calculate total + let totalPrice = 0; + const orderItems = []; + + for (const item of input.items) { + const product = await this.productService.findById(item.productId); + if (!product) { + throw new NotFoundException(`Product ${item.productId} not found`); + } + + if (product.stock < item.quantity) { + throw new BadRequestException( + `Insufficient stock for product ${product.name}` + ); + } + + orderItems.push({ + productId: product.id, + quantity: item.quantity, + priceAtOrder: product.price, + }); + + totalPrice += product.price * item.quantity; + } + + // Create order + const order = await super.create({ + ...input, + items: orderItems, + totalPrice, + customerId: userId, + status: 'pending', + }); + + // Reduce stock + for (const item of input.items) { + await this.productService.reduceStock(item.productId, item.quantity); + } + + return order; +} +``` + +--- + +## Example 3: GraphQL Mutation - User Profile Update + +### Story Requirement + +``` +As a user, I want to update my profile information so that my account reflects current details. + +Acceptance Criteria: +- Users can update their firstName, lastName, phone +- Users cannot change their email through this endpoint +- Users can only update their own profile +- Admin users can update any profile +- Phone number must be validated (German format) +``` + +### Step 2: Create Story Test (GraphQL) + +**File:** `test/stories/profile-update.story.test.ts` + +```typescript +import { + ConfigService, + HttpExceptionLogFilter, + TestGraphQLType, + TestHelper, +} from '@lenne.tech/nest-server'; +import { Test, TestingModule } from '@nestjs/testing'; +import { PubSub } from 'graphql-subscriptions'; +import { MongoClient } from 'mongodb'; + +import envConfig from '../../src/config.env'; +import { RoleEnum } from '../../src/server/common/enums/role.enum'; +import { UserService } from '../../src/server/modules/user/user.service'; +import { imports, ServerModule } from '../../src/server/server.module'; + +describe('Profile Update Story (GraphQL)', () => { + // Test environment properties + let app; + let testHelper: TestHelper; + + // Database + let connection; + let db; + + // Services + let userService: UserService; + + // Global test data + let gNormalUserId: string; + let gNormalUserToken: string; + let gNormalUserEmail: string; + let gOtherUserId: string; + let gOtherUserToken: string; + let gAdminUserId: string; + let gAdminUserToken: string; + + // Track created entities for cleanup + let createdUserIds: string[] = []; + + beforeAll(async () => { + // Start server for testing + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [...imports, ServerModule], + providers: [ + UserService, + { + provide: 'PUB_SUB', + useValue: new PubSub(), + }, + ], + }).compile(); + + app = moduleFixture.createNestApplication(); + app.useGlobalFilters(new HttpExceptionLogFilter()); + app.setBaseViewsDir(envConfig.templates.path); + app.setViewEngine(envConfig.templates.engine); + await app.init(); + + testHelper = new TestHelper(app); + userService = moduleFixture.get(UserService); + + // Connection to database + connection = await MongoClient.connect(envConfig.mongoose.uri); + db = await connection.db(); + + // Create normal user + const normalPassword = Math.random().toString(36).substring(7); + gNormalUserEmail = `user-${normalPassword}@test.com`; + const normalSignUp = await testHelper.graphQl({ + arguments: { + input: { + email: gNormalUserEmail, + firstName: 'John', + lastName: 'Doe', + password: normalPassword, + }, + }, + fields: ['token', { user: ['id', 'email'] }], + name: 'signUp', + type: TestGraphQLType.MUTATION, + }); + gNormalUserId = normalSignUp.user.id; + gNormalUserToken = normalSignUp.token; + + // Track for cleanup + createdUserIds.push(gNormalUserId); + + // Create other user + const otherPassword = Math.random().toString(36).substring(7); + const otherEmail = `other-${otherPassword}@test.com`; + const otherSignUp = await testHelper.graphQl({ + arguments: { + input: { + email: otherEmail, + firstName: 'Other', + password: otherPassword, + }, + }, + fields: ['token', { user: ['id'] }], + name: 'signUp', + type: TestGraphQLType.MUTATION, + }); + gOtherUserId = otherSignUp.user.id; + gOtherUserToken = otherSignUp.token; + + // Track for cleanup + createdUserIds.push(gOtherUserId); + + // Create admin user + const adminPassword = Math.random().toString(36).substring(7); + const adminEmail = `admin-${adminPassword}@test.com`; + const adminSignUp = await testHelper.graphQl({ + arguments: { + input: { + email: adminEmail, + firstName: 'Admin', + password: adminPassword, + }, + }, + fields: ['token', { user: ['id'] }], + name: 'signUp', + type: TestGraphQLType.MUTATION, + }); + gAdminUserId = adminSignUp.user.id; + gAdminUserToken = adminSignUp.token; + + // Track for cleanup + createdUserIds.push(gAdminUserId); + + // Set admin role + await userService.update(gAdminUserId, { roles: [RoleEnum.ADMIN] }, gAdminUserId); + }); + + afterAll(async () => { + // 🧹 CLEANUP: Delete all test data created during tests + try { + // Delete all created users + if (createdUserIds.length > 0) { + await db.collection('users').deleteMany({ + _id: { $in: createdUserIds.map(id => new ObjectId(id)) } + }); + } + } catch (error) { + console.error('Cleanup failed:', error); + } + + await connection.close(); + await app.close(); + }); + + describe('Own Profile Update', () => { + it('should allow user to update own profile', async () => { + const result = await testHelper.graphQl({ + arguments: { + id: gNormalUserId, + input: { + firstName: 'Jane', + lastName: 'Smith', + phone: '+49 123 456789', + }, + }, + fields: ['id', 'firstName', 'lastName', 'phone', 'email'], + name: 'updateUser', + type: TestGraphQLType.MUTATION, + }, { token: gNormalUserToken }); + + expect(result).toMatchObject({ + id: gNormalUserId, + firstName: 'Jane', + lastName: 'Smith', + phone: '+49 123 456789', + email: gNormalUserEmail, // Email unchanged + }); + }); + + it('should prevent user from changing email', async () => { + const result = await testHelper.graphQl({ + arguments: { + id: gNormalUserId, + input: { + firstName: 'John', + email: 'newemail@test.com', // Attempt to change email + }, + }, + fields: ['email'], + name: 'updateUser', + type: TestGraphQLType.MUTATION, + }, { token: gNormalUserToken }); + + // Email should remain unchanged + expect(result.email).toBe(gNormalUserEmail); + }); + }); + + describe('Authorization', () => { + it('should prevent user from updating other user profile', async () => { + const result = await testHelper.graphQl({ + arguments: { + id: gOtherUserId, + input: { + firstName: 'Hacker', + }, + }, + fields: ['id', 'firstName'], + name: 'updateUser', + type: TestGraphQLType.MUTATION, + }, { token: gNormalUserToken, statusCode: 200 }); + + expect(result.errors).toBeDefined(); + expect(result.errors[0].message).toContain('Forbidden'); + }); + + it('should allow admin to update any profile', async () => { + const result = await testHelper.graphQl({ + arguments: { + id: gNormalUserId, + input: { + firstName: 'AdminUpdated', + }, + }, + fields: ['firstName'], + name: 'updateUser', + type: TestGraphQLType.MUTATION, + }, { token: gAdminUserToken }); + + expect(result.firstName).toBe('AdminUpdated'); + }); + }); + + describe('Validation', () => { + it('should reject invalid phone number format', async () => { + const result = await testHelper.graphQl({ + arguments: { + id: gNormalUserId, + input: { + phone: '123', // Invalid format + }, + }, + fields: ['phone'], + name: 'updateUser', + type: TestGraphQLType.MUTATION, + }, { token: gNormalUserToken, statusCode: 200 }); + + expect(result.errors).toBeDefined(); + expect(result.errors[0].message).toContain('phone'); + }); + + it('should accept valid German phone formats', async () => { + const validPhones = [ + '+49 123 456789', + '+49 (0)123 456789', + '0123 456789', + ]; + + for (const phone of validPhones) { + const result = await testHelper.graphQl({ + arguments: { + id: gNormalUserId, + input: { phone }, + }, + fields: ['phone'], + name: 'updateUser', + type: TestGraphQLType.MUTATION, + }, { token: gNormalUserToken }); + + expect(result.phone).toBe(phone); + } + }); + }); +}); +``` + +--- + +## Debugging Test Failures + +When your tests fail and error messages are unclear, enable debugging: + +### TestHelper Debugging Options + +```typescript +// Add to any failing test for detailed output +const result = await testHelper.graphQl({ + arguments: { id: userId }, + fields: ['id', 'email'], + name: 'getUser', + type: TestGraphQLType.MUTATION, +}, { + token: userToken, + log: true, // Logs request details to console + logError: true, // Logs detailed error information +}); + +// Or for REST calls +const result = await testHelper.rest('/api/endpoint', { + method: 'POST', + payload: data, + token: userToken, + log: true, + logError: true, +}); +``` + +### Server-Side Debugging + +**Enable exception logging** in `src/config.env.ts`: +```typescript +export default { + logExceptions: true, // Shows stack traces for all exceptions + // ... other config +}; +``` + +**Enable validation debugging** via environment variable: +```bash +# Run tests with validation debugging +DEBUG_VALIDATION=true npm test +``` + +Or set in your test file: +```typescript +beforeAll(async () => { + // Enable validation debug logging + process.env.DEBUG_VALIDATION = 'true'; + + // ... rest of setup +}); +``` + +This enables detailed console.debug output from MapAndValidatePipe (`node_modules/@lenne.tech/nest-server/src/core/common/pipes/map-and-validate.pipe.ts`). + +### Full Debugging Setup Example + +```typescript +describe('My Story Test', () => { + beforeAll(async () => { + // Enable validation debugging + process.env.DEBUG_VALIDATION = 'true'; + + // ... normal setup + }); + + it('should debug this failing test', async () => { + const result = await testHelper.graphQl({ + // ... your test config + }, { + log: true, // Enable request/response logging + logError: true, // Enable error logging + }); + }); +}); +``` + +**Remember to disable debugging logs before committing** to keep test output clean in CI/CD. + +--- + +## Key Takeaways from Examples + +### 1. Test Structure +- Always setup test data in `beforeAll` +- Clean up in `afterAll` +- Group related tests in `describe` blocks +- Test happy path, validation, authorization separately + +### 2. Security Testing +- Create users with different roles +- Test both authorized and unauthorized access +- Never weaken security to make tests pass +- Test permission boundaries explicitly + +### 3. Business Logic +- Test calculated fields (like totalPrice) +- Test side effects (like stock reduction) +- Test validation rules thoroughly +- Test edge cases and error conditions + +### 4. Implementation Strategy +- Use nest-server-generator for scaffolding +- Implement business logic in services +- Add custom validation where needed +- Follow existing patterns in codebase + +### 5. Debugging +- Use `log: true` and `logError: true` in TestHelper for detailed output +- Enable `logExceptions` in config.env.ts for server-side errors +- Use `DEBUG_VALIDATION=true` for validation debugging +- Disable the debug logs again once all tests have been completed without errors + +### 6. Iteration +- First run will always fail (expected) +- Fix failures systematically +- Enable debugging when error messages are unclear +- Re-run tests after each change +- Continue until all tests pass + +Remember: **Tests define the contract, code fulfills the contract.** \ No newline at end of file diff --git a/src/templates/claude-skills/story-tdd/reference.md b/src/templates/claude-skills/story-tdd/reference.md new file mode 100644 index 0000000..5cf9810 --- /dev/null +++ b/src/templates/claude-skills/story-tdd/reference.md @@ -0,0 +1,1180 @@ +--- +name: story-tdd-reference +version: 1.0.0 +description: Quick reference guide for Test-Driven Development workflow +--- + +# Story-Based TDD Quick Reference + +## The 7-Step Workflow + +``` +┌─────────────────────────────────────────────────────────┐ +│ Step 1: Analyze Story & Clarify │ +│ - Read requirements thoroughly │ +│ - Check existing API structure │ +│ - Identify contradictions │ +│ - ASK DEVELOPER if anything unclear │ +└─────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ Step 2: Create Story Test │ +│ - Location: test/stories/feature-name.story.test.ts │ +│ - Study existing test patterns │ +│ - Write comprehensive test scenarios │ +│ - Cover happy path, errors, edge cases │ +└─────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ Step 3: Run Tests & Analyze │ +│ - npm test │ +│ - Record failures and reasons │ +│ - Decide: Test bug OR Missing implementation │ +└─────────────────────────────────────────────────────────┘ + ↓ + ┌──────┴──────┐ + │ │ + ┌──────────▼─┐ ┌───▼────────────┐ + │ Step 3a: │ │ Step 4: │ + │ Fix Test │ │ Implement Code │ + │ Errors │ │ (Use nest- │ + │ │ │ server- │ + │ │ │ generator) │ + └──────┬─────┘ └───┬────────────┘ + │ │ + └────────┬────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ Step 5: Validate │ +│ - Run ALL tests │ +│ - All pass? → Go to Step 5a │ +│ - Some fail? → Back to Step 3 │ +└─────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ Step 5a: Code Quality, Security & Refactoring Check │ +│ - Check for code duplication │ +│ - Extract common functionality │ +│ - Consolidate similar code paths │ +│ - Review for consistency │ +│ - Check database indexes │ +│ - 🔐 SECURITY REVIEW (CRITICAL) │ +│ - Run tests after refactoring │ +└─────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ Step 5b: Final Validation │ +│ - Run ALL tests one final time │ +│ - Generate report → DONE! 🎉 │ +└─────────────────────────────────────────────────────────┘ +``` + +## Commands Cheatsheet + +### Running Tests + +```bash +# Run all tests +npm test + +# Run specific story test +npm test -- test/stories/feature-name.story.test.ts + +# Run tests with coverage +npm run test:cov + +# Run tests in watch mode +npm run test:watch +``` + +### Using nest-server-generator Skill + +```bash +# Create module +lt server module ModuleName --no-interactive + +# Create object +lt server object ObjectName --no-interactive + +# Add property +lt server addProp ModuleName propertyName:type --no-interactive + +# Examples: +lt server module Review --no-interactive +lt server addProp Review rating:number --no-interactive +lt server addProp Review comment:string? --no-interactive +``` + +## Test File Template + +```typescript +import { + ConfigService, + HttpExceptionLogFilter, + TestGraphQLType, + TestHelper, +} from '@lenne.tech/nest-server'; +import { Test, TestingModule } from '@nestjs/testing'; +import { PubSub } from 'graphql-subscriptions'; +import { MongoClient } from 'mongodb'; + +import envConfig from '../../src/config.env'; +import { RoleEnum } from '../../src/server/common/enums/role.enum'; +import { YourService } from '../../src/server/modules/your-module/your.service'; +import { imports, ServerModule } from '../../src/server/server.module'; + +describe('[Feature Name] Story', () => { + // Test environment properties + let app; + let testHelper: TestHelper; + + // Database + let connection; + let db; + + // Services + let yourService: YourService; + + // Global test data + let gUserToken: string; + let gUserId: string; + + // Track created entities for cleanup + let createdEntityIds: string[] = []; + + beforeAll(async () => { + // Start server for testing + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [...imports, ServerModule], + providers: [ + YourService, + { + provide: 'PUB_SUB', + useValue: new PubSub(), + }, + ], + }).compile(); + + app = moduleFixture.createNestApplication(); + app.useGlobalFilters(new HttpExceptionLogFilter()); + app.setBaseViewsDir(envConfig.templates.path); + app.setViewEngine(envConfig.templates.engine); + await app.init(); + + testHelper = new TestHelper(app); + yourService = moduleFixture.get(YourService); + + // Connection to database + connection = await MongoClient.connect(envConfig.mongoose.uri); + db = await connection.db(); + + // Create test user + const password = Math.random().toString(36).substring(7); + const email = `test-${password}@example.com`; + const signUp = await testHelper.graphQl({ + arguments: { + input: { + email, + firstName: 'Test', + password, + }, + }, + fields: ['token', { user: ['id', 'email'] }], + name: 'signUp', + type: TestGraphQLType.MUTATION, + }); + gUserId = signUp.user.id; + gUserToken = signUp.token; + }); + + afterAll(async () => { + // 🧹 CLEANUP: Delete all test data created during tests + try { + if (createdEntityIds.length > 0) { + await db.collection('entities').deleteMany({ + _id: { $in: createdEntityIds.map(id => new ObjectId(id)) } + }); + } + + // Delete test user + if (gUserId) { + await db.collection('users').deleteOne({ _id: new ObjectId(gUserId) }); + } + } catch (error) { + console.error('Cleanup failed:', error); + } + + await connection.close(); + await app.close(); + }); + + describe('Happy Path', () => { + it('should [expected behavior]', async () => { + // Arrange + const data = { /* test data */ }; + + // Act - Using REST + const result = await testHelper.rest('/api/endpoint', { + method: 'POST', + payload: data, + token: gUserToken, + }); + + // Assert + expect(result).toMatchObject({ + // expected properties + }); + + // Track for cleanup + createdEntityIds.push(result.id); + }); + }); + + describe('Error Cases', () => { + it('should reject invalid input', async () => { + await testHelper.rest('/api/endpoint', { + method: 'POST', + payload: { /* invalid data */ }, + statusCode: 400, + token: gUserToken, + }); + }); + + it('should require authentication', async () => { + await testHelper.rest('/api/endpoint', { + method: 'POST', + payload: { /* data */ }, + statusCode: 401, + }); + }); + }); +}); +``` + +## Database Indexes with @UnifiedField + +### When to Add Indexes + +**🔍 ALWAYS define indexes in @UnifiedField decorator via mongoose option!** + +```typescript +// ✅ CORRECT: Index in decorator mongoose option +@UnifiedField({ + description: 'User email', + mongoose: { index: true, unique: true, type: String } +}) +email: string; + +// ❌ WRONG: Separate schema index (hard to find) +UserSchema.index({ email: 1 }, { unique: true }); +``` + +### Common Index Patterns + +**Single Field Index:** +```typescript +@UnifiedField({ + description: 'Product category', + mongoose: { index: true, type: String } // For queries like: find({ category: 'electronics' }) +}) +category: string; +``` + +**Unique Index:** +```typescript +@UnifiedField({ + description: 'Username', + mongoose: { index: true, unique: true, type: String } // Prevents duplicates +}) +username: string; +``` + +**Foreign Key Index:** +```typescript +@UnifiedField({ + description: 'User who created this', + mongoose: { index: true, type: String } // For JOIN/population operations +}) +createdBy: string; +``` + +**Multiple Indexed Fields:** +```typescript +@UnifiedField({ + description: 'Customer reference', + mongoose: { index: true, type: String } // Indexed individually +}) +customerId: string; + +@UnifiedField({ + description: 'Order status', + mongoose: { index: true, type: String } // Indexed individually +}) +status: string; + +// Both indexed for flexible querying +``` + +**Text Search Index:** +```typescript +@UnifiedField({ + description: 'Product name', + mongoose: { type: String, text: true } // For full-text search +}) +name: string; +``` + +### Index Checklist + +Before marking complete, verify: + +- [ ] Fields used in `find()` queries have indexes +- [ ] Foreign keys (userId, productId, etc.) have indexes +- [ ] Unique fields (email, username) marked with `unique: true` +- [ ] Fields used in sorting have indexes +- [ ] Compound queries use compound indexes +- [ ] All indexes in @UnifiedField decorator (NOT separate schema) + +## REST API Testing Patterns (using TestHelper) + +```typescript +// GET request +const result = await testHelper.rest('/api/resource/123', { + token: userToken, +}); + +// GET request (public endpoint, no auth) +const result = await testHelper.rest('/api/public'); + +// POST request +const result = await testHelper.rest('/api/resource', { + method: 'POST', + payload: data, + token: userToken, +}); + +// PUT request +const result = await testHelper.rest('/api/resource/123', { + method: 'PUT', + payload: updates, + token: userToken, +}); + +// DELETE request +const result = await testHelper.rest('/api/resource/123', { + method: 'DELETE', + token: userToken, +}); + +// Expect specific status code +await testHelper.rest('/api/resource', { + method: 'POST', + payload: invalidData, + statusCode: 400, + token: userToken, +}); + +// With custom headers +const result = await testHelper.rest('/api/resource', { + headers: { + 'Content-Type': 'application/json', + 'X-Custom-Header': 'value', + }, + token: userToken, +}); +``` + +## GraphQL Testing Patterns (using TestHelper) + +```typescript +import { TestGraphQLType, TestHelper } from '@lenne.tech/nest-server'; + +// GraphQL Query +const user = await testHelper.graphQl({ + arguments: { + id: userId, + }, + fields: ['id', 'email', 'firstName', { profile: ['bio', 'avatar'] }], + name: 'getUser', + type: TestGraphQLType.QUERY, +}, { token: userToken }); + +expect(user).toMatchObject({ + id: userId, + email: 'test@example.com', +}); + +// GraphQL Mutation +const result = await testHelper.graphQl({ + arguments: { + input: { + firstName: 'Updated', + lastName: 'Name', + }, + }, + fields: ['id', 'firstName', 'lastName'], + name: 'updateUser', + type: TestGraphQLType.MUTATION, +}, { token: userToken }); + +// GraphQL Mutation with nested objects +const created = await testHelper.graphQl({ + arguments: { + input: { + title: 'New Post', + content: 'Post content', + tags: ['tag1', 'tag2'], + }, + }, + fields: ['id', 'title', { author: ['id', 'email'] }, 'tags'], + name: 'createPost', + type: TestGraphQLType.MUTATION, +}, { token: userToken }); + +// GraphQL Query without auth (public) +const publicData = await testHelper.graphQl({ + arguments: {}, + fields: ['version', 'status'], + name: 'getPublicInfo', + type: TestGraphQLType.QUERY, +}); + +// Expecting errors (e.g., unauthorized) +const result = await testHelper.graphQl({ + arguments: { id: otherUserId }, + fields: ['id', 'email'], + name: 'getUser', + type: TestGraphQLType.QUERY, +}, { token: userToken, statusCode: 200 }); + +expect(result.errors).toBeDefined(); +expect(result.errors[0].message).toContain('Forbidden'); +``` + +## Common Test Assertions + +```typescript +// Object matching +expect(result).toMatchObject({ key: value }); + +// Exact equality +expect(result).toEqual(expected); + +// Array checks +expect(array).toHaveLength(3); +expect(array).toContain(item); +expect(array).toBeInstanceOf(Array); + +// Existence checks +expect(value).toBeDefined(); +expect(value).toBeUndefined(); +expect(value).toBeNull(); +expect(value).toBeTruthy(); +expect(value).toBeFalsy(); + +// Number comparisons +expect(number).toBeGreaterThan(5); +expect(number).toBeLessThan(10); +expect(number).toBeCloseTo(3.14, 2); + +// String matching +expect(string).toContain('substring'); +expect(string).toMatch(/regex/); + +// Error checking +expect(() => fn()).toThrow(); +expect(() => fn()).toThrow('error message'); +``` + +## Security Testing Checklist + +```typescript +// ✅ Create users with correct roles using TestHelper +const userSignUp = await testHelper.graphQl({ + arguments: { + input: { + email: 'user@test.com', + password: 'password123', + firstName: 'Test', + }, + }, + fields: ['token', { user: ['id'] }], + name: 'signUp', + type: TestGraphQLType.MUTATION, +}); +const userToken = userSignUp.token; + +// ✅ Test with correct role +await testHelper.rest('/api/resource', { + token: userToken, +}); + +// ✅ Test without authentication +await testHelper.rest('/api/resource', { + statusCode: 401, +}); + +// ✅ Test with insufficient permissions +await testHelper.rest('/api/admin/resource', { + statusCode: 403, + token: userToken, // Normal user trying admin endpoint +}); + +// ✅ Test access to own resources only +await testHelper.rest(`/api/users/${userSignUp.user.id}/profile`, { + method: 'PUT', + payload: { firstName: 'Updated' }, + token: userToken, +}); + +await testHelper.rest(`/api/users/${otherUserId}/profile`, { + method: 'PUT', + payload: { firstName: 'Hacker' }, + statusCode: 403, + token: userToken, +}); + +// ❌ NEVER do this +// Don't remove @Restricted decorators +// Don't change @Roles to more permissive +// Don't disable security checks +``` + +## When to Ask Developer + +``` +❓ ASK when: +- Story has contradictions or ambiguities +- Need to change security decorators +- Need to add new npm package +- Multiple valid architectural approaches +- Tests keep failing for unclear reasons + +✅ DON'T ASK when: +- Creating test files +- Running tests +- Analyzing failures +- Implementing obvious features +- Using nest-server-generator +``` + +## Debugging Failed Tests + +When tests fail, use these debugging tools to analyze the issue: + +### 1. TestHelper Logging Options + +```typescript +// Enable detailed request/response logging +const result = await testHelper.graphQl({ + arguments: { id: userId }, + fields: ['id', 'email'], + name: 'getUser', + type: TestGraphQLType.QUERY, +}, { + token: userToken, + log: true, // Logs request details + logError: true, // Logs errors when status >= 400 +}); + +// For REST requests +const result = await testHelper.rest('/api/endpoint', { + method: 'POST', + payload: data, + token: userToken, + log: true, + logError: true, +}); +``` + +### 2. Server Exception Logging + +Enable in `src/config.env.ts`: + +```typescript +export default { + // ... other config + logExceptions: true, // Log all exceptions with stack traces + // ... +}; +``` + +### 3. Validation Debug Logging + +Enable validation debugging via environment variable: + +```bash +# In your terminal or test script +DEBUG_VALIDATION=true npm test + +# Or in your test file +process.env.DEBUG_VALIDATION = 'true'; +``` + +This activates console.debug statements in MapAndValidatePipe (`node_modules/@lenne.tech/nest-server/src/core/common/pipes/map-and-validate.pipe.ts`) to show detailed validation errors. + +### 4. Combined Debugging Setup + +For comprehensive debugging, combine all three: + +```typescript +// In your test file beforeAll +process.env.DEBUG_VALIDATION = 'true'; + +// In src/config.env.ts +export default { + logExceptions: true, + // ... +}; + +// In your tests +const result = await testHelper.graphQl({ + // ... your test +}, { + log: true, + logError: true, +}); +``` + +## Decision Tree: Test Failure Analysis + +``` +Test fails + │ + ├─► Missing implementation? + │ └─► Go to Step 4 (Implement) + │ + ├─► Test has bugs/errors? + │ └─► Go to Step 3a (Fix test) + │ + ├─► Security blocking correctly? + │ └─► Fix test to use proper auth + │ + ├─► Unclear error message? + │ └─► Enable debugging (log, logError, logExceptions, DEBUG_VALIDATION) + │ + └─► Still unclear why failing? + └─► Ask developer +``` + +## Code Quality, Security & Refactoring Check + +### Quick Review Guide + +**Before marking complete, check for:** + +1. **Code Duplication:** + - Repeated validation logic → Extract to private method + - Similar calculations in multiple places → Create helper function + - Duplicated query patterns → Consolidate into flexible method + +2. **Common Functionality:** + - Extract repeated data transformations + - Create shared validation helpers + - Consolidate similar query builders + +3. **Database Indexes:** + - Fields used in queries → Add `mongoose: { index: true, type: String }` to @UnifiedField + - Foreign keys → Add index via mongoose option + - Unique fields → Add `mongoose: { index: true, unique: true, type: String }` + - Multiple query fields → Index each individually + +4. **🔐 Security Review (CRITICAL):** + - @Restricted/@Roles decorators NOT removed or weakened + - Ownership checks in place for user data + - All inputs validated with DTOs + - Sensitive fields marked with `hideField: true` + - No injection vulnerabilities + - Error messages don't leak sensitive data + - Authorization tests pass + +5. **Refactoring Decision:** + ``` + Used in 2+ places? → Extract to private method + Used across services? → Consider utility class + Only 1 usage? → Leave as-is (don't over-engineer) + ``` + +6. **After Refactoring & Security Review:** + ```bash + npm test # MUST still pass! + ``` + +### Code Quality Checklist + +Before marking complete: + +- [ ] All tests passing +- [ ] **No obvious code duplication** +- [ ] **Common functionality extracted to helpers** +- [ ] **Consistent patterns throughout** +- [ ] **Database indexes added to @UnifiedField decorators** +- [ ] **Indexes match query patterns in services** +- [ ] Test coverage adequate (80%+) +- [ ] Code follows existing patterns +- [ ] No unnecessary dependencies added +- [ ] Proper error handling +- [ ] Input validation implemented +- [ ] Documentation/comments where needed +- [ ] **Tests still pass after refactoring** + +**🔐 Security Checklist:** + +- [ ] **@Restricted/@Roles decorators NOT removed or weakened** +- [ ] **Ownership checks in place (users can only access own data)** +- [ ] **All inputs validated with proper DTOs** +- [ ] **Sensitive fields marked with hideField: true** +- [ ] **No SQL/NoSQL injection vulnerabilities** +- [ ] **Error messages don't expose sensitive data** +- [ ] **checkSecurity methods implemented in models** +- [ ] **Authorization tests pass** +- [ ] **No hardcoded secrets or credentials** + +## Final Report Template + +```markdown +# Story Implementation Complete ✅ + +## Story: [Name] + +### Tests Created +- Location: test/stories/[filename].story.test.ts +- Test cases: X scenarios +- Coverage: X% + +### Implementation Summary +- Modules: [list] +- Objects: [list] +- Properties: [list] +- Other: [list] + +### Test Results +✅ All X tests passing + +### Code Quality +- Patterns followed: ✅ +- Security preserved: ✅ +- Dependencies: None added ✅ +- Code duplication checked: ✅ +- Database indexes added: ✅ +- Refactoring performed: [Yes/No] + +### Security Review +- Authentication/Authorization: ✅ +- Input validation: ✅ +- Data exposure prevented: ✅ +- Ownership checks: ✅ +- Injection prevention: ✅ +- Authorization tests pass: ✅ + +### Refactoring (if performed) +- Extracted helper functions: [list] +- Consolidated code paths: [describe] +- Removed duplication: [describe] +- Tests still passing: ✅ + +### Files Modified +1. path/to/file.ts - description +2. path/to/file.ts - description +``` + +## 🔄 Handling Existing Tests + +**When your changes break existing tests:** + +### Decision Tree + +``` +Existing test fails + ├─► Intentional breaking change? (e.g., added required field) + │ └─► ✅ Update test to match new behavior + │ + └─► Unclear/unintended side effect? + ├─► 🔍 Use git to investigate: + │ - git show HEAD:path/to/file.ts + │ - git diff HEAD path/to/file.ts + │ + └─► ⚠️ Fix code to satisfy BOTH old AND new tests +``` + +### Git Analysis (ALLOWED) + +```bash +# View old version of file +git show HEAD:src/server/modules/user/user.service.ts + +# See what changed +git diff HEAD src/server/modules/user/user.service.ts + +# View commit history +git log -p --follow path/to/file.ts +``` + +### Guidelines + +**✅ Update tests when:** +- Intentional API contract change +- Removed deprecated functionality +- Renamed fields/methods +- Documented in story requirements + +**❌ Don't update tests when:** +- Unclear why they're failing +- Unrelated to your story +- Multiple unrelated tests breaking +- Testing important existing functionality + +**🚩 Red flags (investigate, don't update):** +- Tests in different modules failing +- Security/auth tests failing +- 3+ unrelated tests failing + +**Remember:** +- Existing tests = documentation of expected behavior +- Use git freely for investigation (NOT commits!) +- When in doubt, preserve backward compatibility + +## ⛔ CRITICAL: Git Commits + +**🚨 NEVER create git commits unless explicitly requested by the developer.** + +- ❌ DO NOT use `git add`, `git commit`, or `git push` automatically +- ❌ DO NOT commit changes when tests pass +- ❌ DO NOT assume developer wants changes committed +- ✅ ONLY commit when developer explicitly asks: "commit these changes" + +**Why:** Developers may want to review changes, commit in specific chunks, or have custom workflows. + +**Your job:** +- ✅ Create/modify files +- ✅ Run tests +- ✅ Use git for analysis (git show, git diff, git log) +- ✅ Provide comprehensive report +- ❌ Never commit to git (unless explicitly requested) + +## 🚨 CRITICAL: Database Cleanup & Test Isolation + +**ALWAYS implement comprehensive cleanup in your story tests!** + +Test data that remains in the database can cause: +- False positives/negatives in tests +- Flaky tests that pass/fail randomly +- Contaminated test database +- Hard-to-debug test failures + +### Between Test Suites - RECOMMENDED APPROACH + +**Track all created entities and delete them explicitly:** + +```typescript +describe('Feature Story', () => { + // Track created entities + let createdUserIds: string[] = []; + let createdProductIds: string[] = []; + let createdOrderIds: string[] = []; + + // In your tests, track IDs immediately after creation + it('should create product', async () => { + const product = await testHelper.rest('/api/products', { + method: 'POST', + payload: productData, + token: adminToken, + }); + + // ✅ Track for cleanup + createdProductIds.push(product.id); + }); + + afterAll(async () => { + // 🧹 CLEANUP: Delete ALL test data created during tests + try { + // Delete in correct order (child entities first) + if (createdOrderIds.length > 0) { + await db.collection('orders').deleteMany({ + _id: { $in: createdOrderIds.map(id => new ObjectId(id)) } + }); + } + + if (createdProductIds.length > 0) { + await db.collection('products').deleteMany({ + _id: { $in: createdProductIds.map(id => new ObjectId(id)) } + }); + } + + if (createdUserIds.length > 0) { + await db.collection('users').deleteMany({ + _id: { $in: createdUserIds.map(id => new ObjectId(id)) } + }); + } + } catch (error) { + console.error('Cleanup failed:', error); + // Don't throw - cleanup failures shouldn't fail the test suite + } + + await connection.close(); + await app.close(); + }); +}); +``` + +### Alternative: Pattern-Based Cleanup (Less Reliable) + +Only use this if you can't track IDs: + +```typescript +afterAll(async () => { + // Clean up test data by pattern (less reliable) + await db.collection('users').deleteMany({ email: /@test\.com$/ }); + await db.collection('products').deleteMany({ name: /^Test/ }); + + await connection.close(); + await app.close(); +}); +``` + +**⚠️ Warning:** Pattern-based cleanup can: +- Miss entities that don't match the pattern +- Delete unintended entities if pattern is too broad +- Be harder to debug when cleanup fails + +### Between Individual Tests + +Use `beforeEach`/`afterEach` only when necessary: + +```typescript +describe('Feature Tests', () => { + let sharedResource; + + beforeEach(async () => { + // Reset state before each test if needed + sharedResource = await createFreshResource(); + }); + + afterEach(async () => { + // Clean up after each test if needed + await deleteResource(sharedResource.id); + }); +}); +``` + +## User Authentication: signUp vs signIn + +### When to use signUp + +- Creating new users in tests +- Full control over user data needed +- Testing user registration flows +- Most common in story tests + +```typescript +const signUp = await testHelper.graphQl({ + arguments: { + input: { + email: `test-${Date.now()}@example.com`, // Unique email + password: 'testpass123', + firstName: 'Test', + }, + }, + fields: ['token', { user: ['id', 'email'] }], + name: 'signUp', + type: TestGraphQLType.MUTATION, +}); +const token = signUp.token; +``` + +### When to use signIn + +- Authenticating existing users +- User already exists in database +- Testing login flows + +```typescript +const signIn = await testHelper.rest('/auth/signin', { + method: 'POST', + payload: { + email: existingUserEmail, + password: existingUserPassword, + }, +}); +const token = signIn.token; +``` + +## Avoiding Test Interdependencies + +### ❌ DON'T: Shared state between tests + +```typescript +// ❌ BAD: Test 2 depends on Test 1 +let createdUserId; + +it('should create user', async () => { + const user = await createUser(...); + createdUserId = user.id; // ❌ Shared state! +}); + +it('should update user', async () => { + await updateUser(createdUserId, ...); // ❌ Depends on Test 1! +}); +``` + +### ✅ DO: Independent tests + +```typescript +// ✅ GOOD: Each test is independent +describe('User CRUD', () => { + let testUserId; + + beforeEach(async () => { + // Create fresh user for EACH test + const user = await createUser(...); + testUserId = user.id; + }); + + afterEach(async () => { + // Clean up after each test + await deleteUser(testUserId); + }); + + it('should update user', async () => { + await updateUser(testUserId, ...); // ✅ Independent! + }); + + it('should delete user', async () => { + await deleteUser(testUserId, ...); // ✅ Independent! + }); +}); +``` + +## Async/Await Best Practices + +### Always await async operations + +```typescript +// ❌ WRONG: Forgotten await +const user = testHelper.graphQl({...}); // Returns Promise, not user! +expect(user.email).toBe('test@example.com'); // FAILS! + +// ✅ CORRECT: With await +const user = await testHelper.graphQl({...}); +expect(user.email).toBe('test@example.com'); // Works! +``` + +### Parallel vs Sequential execution + +```typescript +// ✅ Parallel execution (independent operations) +const [user1, user2, product] = await Promise.all([ + testHelper.graphQl({...}), // Create user 1 + testHelper.graphQl({...}), // Create user 2 + testHelper.rest('/api/products', {...}), // Create product +]); + +// ✅ Sequential execution (dependent operations) +const user = await testHelper.graphQl({...}); +const product = await testHelper.rest('/api/products', { + token: user.token, // Depends on user being created first + payload: {...}, + method: 'POST', +}); + +// ❌ WRONG: Sequential when parallel is possible (slower) +const user1 = await testHelper.graphQl({...}); +const user2 = await testHelper.graphQl({...}); // Could run in parallel! +const product = await testHelper.rest('/api/products', {...}); +``` + +### Handling errors with async/await + +```typescript +// Test that async operation throws error +await expect(async () => { + await testHelper.rest('/api/resource', { + payload: invalidData, + token: userToken, + }); +}).rejects.toThrow(); + +// Or use statusCode option +await testHelper.rest('/api/resource', { + payload: invalidData, + statusCode: 400, + token: userToken, +}); +``` + +## Common Pitfalls to Avoid + +❌ **Don't:** +- Write code before tests +- Skip test analysis step +- **Weaken security for passing tests** +- **Remove or weaken @Restricted/@Roles decorators** +- **Skip security review before marking complete** +- Add dependencies without checking existing +- Ignore existing code patterns +- Batch test completions (mark complete immediately) +- Work on multiple tasks simultaneously +- **Create git commits without explicit request** +- Forget `await` on async calls +- Create test interdependencies +- **Forget to implement cleanup in afterAll** +- **Forget to track created entity IDs for cleanup** +- Clean up too aggressively (breaking other tests) +- Use pattern-based cleanup when ID tracking is possible +- **Skip code quality check before marking complete** +- **Leave obvious code duplication in place** +- Over-engineer by extracting single-use code +- **Define indexes separately in schema files** +- **Forget to add indexes for queried fields** +- Add indexes to fields that are never queried +- **Expose sensitive fields without hideField** +- **Allow users to access others' data without checks** +- **Use 'any' type instead of proper DTOs** + +✅ **Do:** +- Follow the 7-step process strictly (including Step 5a security & refactoring check) +- Ask for clarification early +- **Preserve all security mechanisms (CRITICAL)** +- **Perform security review before marking complete** +- Study existing code first +- Match existing patterns +- Mark todos complete as you finish them +- Focus on one step at a time +- **Wait for developer to commit changes** +- Always use `await` with async operations +- Make tests independent +- Use `beforeEach`/`afterEach` for test isolation +- Use Promise.all() for parallel operations +- **ALWAYS implement comprehensive cleanup in afterAll** +- **Track all created entity IDs immediately after creation** +- Delete entities in correct order (children before parents) +- **Check for code duplication before marking complete** +- **Extract common functionality to helpers when used 2+ times** +- **Run tests again after refactoring** +- **Verify ownership checks for user data access** +- **Mark sensitive fields with hideField: true** +- **Use proper DTOs with validation decorators** +- **Ensure authorization tests pass** + +## Integration Points + +### With nest-server-generator +- Use for creating modules, objects, properties +- Use for understanding NestJS patterns +- Use for reading CrudService implementations + +### With Existing Tests +- Study patterns in test/ directory +- Copy authentication setup approach +- Use same helper functions +- Match assertion style + +### With API Documentation +- Check Controllers for REST endpoints +- Review Swagger annotations +- Understand existing data models +- Verify GraphQL schema if applicable + +--- + +**Remember:** Tests first, code second. Iterate until green. **Security review mandatory.** Refactor before done. Quality over speed. \ No newline at end of file diff --git a/src/templates/claude-skills/story-tdd/security-review.md b/src/templates/claude-skills/story-tdd/security-review.md new file mode 100644 index 0000000..e91ff81 --- /dev/null +++ b/src/templates/claude-skills/story-tdd/security-review.md @@ -0,0 +1,299 @@ +--- +name: story-tdd-security-review +version: 1.0.0 +description: Security review checklist for Test-Driven Development - ensures no vulnerabilities are introduced +--- + +# 🔐 Security Review Checklist + +**CRITICAL: Perform security review before final testing!** + +**ALWAYS review all code changes for security vulnerabilities before marking complete.** + +Security issues can be introduced during implementation without realizing it. A systematic review prevents: +- Unauthorized access to data +- Privilege escalation +- Data leaks +- Injection attacks +- Authentication bypasses + +--- + +## Security Checklist + +### 1. Authentication & Authorization + +✅ **Check decorators are NOT weakened:** + +```typescript +// ❌ WRONG: Removing security to make tests pass +// OLD: +@Restricted(RoleEnum.ADMIN) +async deleteUser(id: string) { ... } + +// NEW (DANGEROUS): +async deleteUser(id: string) { ... } // ⚠️ No restriction! + +// ✅ CORRECT: Keep or strengthen security +@Restricted(RoleEnum.ADMIN) +async deleteUser(id: string) { ... } +``` + +✅ **Verify @Roles decorators:** + +```typescript +// ❌ WRONG: Making endpoint too permissive +@Roles(RoleEnum.S_USER) // Everyone can delete! +async deleteOrder(id: string) { ... } + +// ✅ CORRECT: Proper role restriction +@Roles(RoleEnum.ADMIN) // Only admins can delete +async deleteOrder(id: string) { ... } +``` + +✅ **Check ownership verification:** + +```typescript +// ❌ WRONG: No ownership check +async updateProfile(userId: string, data: UpdateProfileInput, currentUser: User) { + return this.userService.update(userId, data); // Any user can update any profile! +} + +// ✅ CORRECT: Verify ownership or admin role +async updateProfile(userId: string, data: UpdateProfileInput, currentUser: User) { + // Check if user is updating their own profile or is admin + if (userId !== currentUser.id && !currentUser.roles.includes(RoleEnum.ADMIN)) { + throw new ForbiddenException('Cannot update other users'); + } + return this.userService.update(userId, data); +} +``` + +### 2. Input Validation + +✅ **Verify all inputs are validated:** + +```typescript +// ❌ WRONG: No validation +async createProduct(input: any) { + return this.productService.create(input); // Dangerous! +} + +// ✅ CORRECT: Proper DTO with validation +export class CreateProductInput { + @UnifiedField({ + description: 'Product name', + isOptional: false, + mongoose: { type: String, required: true, minlength: 1, maxlength: 100 } + }) + name: string; + + @UnifiedField({ + description: 'Price', + isOptional: false, + mongoose: { type: Number, required: true, min: 0 } + }) + price: number; +} +``` + +✅ **Check for injection vulnerabilities:** + +```typescript +// ❌ WRONG: Direct string interpolation in queries +async findByName(name: string) { + return this.productModel.find({ $where: `this.name === '${name}'` }); // SQL Injection! +} + +// ✅ CORRECT: Parameterized queries +async findByName(name: string) { + return this.productModel.find({ name }); // Safe +} +``` + +### 3. Data Exposure + +✅ **Verify sensitive data is protected:** + +```typescript +// ❌ WRONG: Exposing passwords +export class User { + @UnifiedField({ description: 'Email' }) + email: string; + + @UnifiedField({ description: 'Password' }) + password: string; // ⚠️ Will be exposed in API! +} + +// ✅ CORRECT: Hide sensitive fields +export class User { + @UnifiedField({ description: 'Email' }) + email: string; + + @UnifiedField({ + description: 'Password hash', + hideField: true, // ✅ Never expose in API + mongoose: { type: String, required: true } + }) + password: string; +} +``` + +✅ **Check error messages don't leak data:** + +```typescript +// ❌ WRONG: Exposing sensitive info in errors +catch (error) { + throw new BadRequestException(`Query failed: ${error.message}, SQL: ${query}`); +} + +// ✅ CORRECT: Generic error messages +catch (error) { + this.logger.error(`Query failed: ${error.message}`, error.stack); + throw new BadRequestException('Invalid request'); +} +``` + +### 4. Authorization in Services + +✅ **Verify service methods check permissions:** + +```typescript +// ❌ WRONG: Service doesn't check who can access +async getOrder(orderId: string) { + return this.orderModel.findById(orderId); // Anyone can see any order! +} + +// ✅ CORRECT: Service checks ownership or role +async getOrder(orderId: string, currentUser: User) { + const order = await this.orderModel.findById(orderId); + + // Check if user owns the order or is admin + if (order.customerId !== currentUser.id && !currentUser.roles.includes(RoleEnum.ADMIN)) { + throw new ForbiddenException('Access denied'); + } + + return order; +} +``` + +### 5. Security Model Checks + +✅ **Verify checkSecurity methods:** + +```typescript +// In model file +async checkSecurity(user: User, mode: SecurityMode): Promise { + // ❌ WRONG: No security check + return; + + // ✅ CORRECT: Proper security implementation + if (mode === SecurityMode.CREATE && !user.roles.includes(RoleEnum.ADMIN)) { + throw new ForbiddenException('Only admins can create'); + } + + if (mode === SecurityMode.UPDATE && this.createdBy !== user.id && !user.roles.includes(RoleEnum.ADMIN)) { + throw new ForbiddenException('Can only update own items'); + } +} +``` + +### 6. Cross-Cutting Concerns + +✅ **Rate limiting for sensitive endpoints:** +- Password reset endpoints +- Authentication endpoints +- Payment processing +- Email sending + +✅ **HTTPS/TLS enforcement (production)** + +✅ **Proper CORS configuration** + +✅ **No hardcoded secrets or API keys** + +--- + +## Security Decision Tree + +``` +Code changes made? + │ + ├─► Modified @Restricted or @Roles? + │ └─► ⚠️ CRITICAL: Verify this was intentional and justified + │ + ├─► New endpoint added? + │ └─► ✅ Ensure proper authentication + authorization decorators + │ + ├─► Service method modified? + │ └─► ✅ Verify ownership checks still in place + │ + ├─► New input/query parameters? + │ └─► ✅ Ensure validation and sanitization + │ + └─► Sensitive data accessed? + └─► ✅ Verify access control and data hiding +``` + +--- + +## Red Flags - STOP and Review + +🚩 **Authentication/Authorization:** +- @Restricted decorator removed or changed +- @Roles changed to more permissive role +- Endpoints without authentication +- Missing ownership checks + +🚩 **Data Security:** +- Sensitive fields not marked with hideField +- Password or token fields exposed +- User data accessible without permission check +- Error messages revealing internal details + +🚩 **Input Validation:** +- Missing validation decorators +- Any type used instead of DTO +- Direct use of user input in queries +- No sanitization of string inputs + +🚩 **Business Logic:** +- Bypassing security checks "for convenience" +- Commented out authorization code +- Admin-only actions available to regular users +- Price/amount manipulation possible + +--- + +## If ANY Red Flag Found + +1. **STOP implementation** +2. **Fix the security issue immediately** +3. **Review surrounding code for similar issues** +4. **Re-run security checklist** +5. **Update tests to verify security works** + +--- + +## Remember + +- **Security > Convenience** +- **Better to over-restrict than under-restrict** +- **Always preserve existing security mechanisms** +- **When in doubt, ask the developer** + +--- + +## Quick Security Checklist + +Before marking complete: + +- [ ] **@Restricted/@Roles decorators NOT removed or weakened** +- [ ] **Ownership checks in place (users can only access own data)** +- [ ] **All inputs validated with proper DTOs** +- [ ] **Sensitive fields marked with hideField: true** +- [ ] **No SQL/NoSQL injection vulnerabilities** +- [ ] **Error messages don't expose sensitive data** +- [ ] **checkSecurity methods implemented in models** +- [ ] **Authorization tests pass** +- [ ] **No hardcoded secrets or credentials**