import { strings } from '@angular-devkit/core'; import { MergeStrategy, SchematicsException, apply, applyTemplates, filter, mergeWith, move, url, } from '@angular-devkit/schematics'; import { createProjectSchematic } from '@schematics/angular/utility/project'; import { parseName } from '@schematics/angular/utility/parse-name'; import { validateClassName } from '@schematics/angular/utility/validation'; import { buildDefaultPath } from '@schematics/angular/utility/workspace'; import { ApiOperation, Schema } from './schema'; const ALLOWED_OPERATIONS = new Set(['list', 'get', 'create', 'update', 'delete']); export const apiResource = createProjectSchematic((options, { project }) => { options.path ??= buildDefaultPath(project); const parsedPath = parseName(options.path, options.name); const name = parsedPath.name; const entity = strings.dasherize(options.entity?.trim() || singularize(name)); const operations = normalizeOperations(options.operations); const models = options.models ?? true; const mapper = (options.mapper ?? true) && models; const baseUrl = normalizeBaseUrl(options.baseUrl ?? `/api/${strings.dasherize(name)}`); validateClassName(`${strings.classify(name)}Api`); validateClassName(strings.classify(entity)); const source = apply(url('./files'), [ filter((filePath) => { if (!models && (filePath.endsWith('.model.ts.template') || filePath.endsWith('.dto.ts.template'))) { return false; } if (!mapper && filePath.endsWith('.mapper.ts.template')) { return false; } return true; }), applyTemplates({ ...strings, name, entity, baseUrl, baseUrlLiteral: JSON.stringify(baseUrl), operations, models, mapper, hasList: operations.includes('list'), hasGet: operations.includes('get'), hasCreate: operations.includes('create'), hasUpdate: operations.includes('update'), hasDelete: operations.includes('delete'), hasMappedResult: mapper && operations.some((operation) => operation !== 'delete'), }), move(parsedPath.path), ]); return mergeWith(source, options.force ? MergeStrategy.Overwrite : MergeStrategy.Default); }); function normalizeOperations(operations: ApiOperation[] | undefined): ApiOperation[] { const normalized = operations ?? ['list', 'get']; if (normalized.length === 0) { throw new SchematicsException('Select at least one API operation.'); } for (const operation of normalized) { if (!ALLOWED_OPERATIONS.has(operation)) { throw new SchematicsException(`Unsupported API operation "${operation}".`); } } return [...new Set(normalized)]; } function normalizeBaseUrl(baseUrl: string): string { const normalized = baseUrl.trim().replace(/\/+$/g, ''); if (!normalized) { throw new SchematicsException('API base URL cannot be empty.'); } return normalized; } function singularize(value: string): string { const dasherized = strings.dasherize(value); if (dasherized.endsWith('ies') && dasherized.length > 3) { return `${dasherized.slice(0, -3)}y`; } if (dasherized.endsWith('s') && !dasherized.endsWith('ss') && dasherized.length > 1) { return dasherized.slice(0, -1); } return dasherized; }