/** * cli:scaffold-coded-entity — generate.ts * * Renders one `{Entity}CodeKeyDescriptor.cs` per coded entity and patches the * DI host idempotently between the `<<< CODED-ENTITY-KEYS-DI >>>` markers * (full-spec replacement — the same discipline as scaffold-data-scope). * Descriptor types are referenced FULLY-QUALIFIED in the patched DI so no * per-module using is needed; only the `AddSmartStackCodeKey` namespace is * ensured. */ import { applicationDir, applicationNs } from '../../../../../lib/app-classification.js'; import { CODE_GENERATION_DI_NAMESPACE, DI_BEGIN_MARKER, DI_END_MARKER, type CodedEntityEntry, type CodedEntitySpec, type GeneratedFile, } from './types.js'; /** Namespace of an entity's descriptor — `{ns}.Application.{App}.{Module}.CodeGeneration`. */ export function descriptorNs(spec: CodedEntitySpec, e: CodedEntityEntry): string { return `${applicationNs(spec.appCode, e.applicationCode, e.module)}.CodeGeneration`; } /** Fully-qualified descriptor type name, as referenced from the patched DI. */ export function descriptorFqType(spec: CodedEntitySpec, e: CodedEntityEntry): string { return `${descriptorNs(spec, e)}.${e.entityName}CodeKeyDescriptor`; } /** * Fully-qualified probe type name (`probeType` input) — a simple name is * qualified into the descriptor's namespace (the convention: the hand-written * `ICodeUniquenessProbe` lives next to its descriptor); a dotted name is * honoured verbatim. */ export function probeFqType(spec: CodedEntitySpec, e: CodedEntityEntry): string | null { if (!e.probeType) return null; return e.probeType.includes('.') ? e.probeType : `${descriptorNs(spec, e)}.${e.probeType}`; } function escapeString(s: string): string { return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); } /** Render one `{Entity}CodeKeyDescriptor.cs`. */ export function renderDescriptor(spec: CodedEntitySpec, e: CodedEntityEntry): GeneratedFile { const content = `using SmartStack.Application.Common.CodeGeneration; using SmartStack.Domain.CodeGeneration; namespace ${descriptorNs(spec, e)}; /// /// Built-in default pattern for ${e.entityName} business codes (key /// "${e.codeKey}" — must equal the entity's ICodedEntity.CodeKey). /// Registered via AddSmartStackCodeKey: no seed, no migration — a DB /// CodePattern row only ever OVERRIDES these defaults, and the key /// surfaces in Administration → Configuration → Code patterns. /// public sealed class ${e.entityName}CodeKeyDescriptor : ICodeKeyDescriptor { public string Key => "${e.codeKey}"; public string Label => "${escapeString(e.label)}"; public string? Description => ${e.description ? `"${escapeString(e.description)}"` : 'null'}; public string DefaultFormat => "${escapeString(e.defaultFormat)}"; public CodeScopeKind DefaultScopeKind => CodeScopeKind.${e.scopeKind}; public CodeResetPeriod DefaultReset => CodeResetPeriod.${e.reset}; public bool Gapless => ${e.gapless ? 'true' : 'false'}; public CollisionStrategy CollisionStrategy => CollisionStrategy.${e.collisionStrategy}; } `; const dir = `${applicationDir(spec.appCode, e.applicationCode, e.module)}/CodeGeneration`; return { path: `${dir}/${e.entityName}CodeKeyDescriptor.cs`, content }; } /** All descriptor files for the spec. */ export function renderDescriptors(spec: CodedEntitySpec): GeneratedFile[] { return spec.entities.map(e => renderDescriptor(spec, e)); } // ─── DI patch ─── const DI_IND = ' '; /** Insert `using {ns};` after the last using directive when it is missing. */ export function ensureUsing(source: string, ns: string): string { const present = new RegExp(`^using\\s+${ns.replace(/\./g, '\\.')}\\s*;`, 'm'); if (present.test(source)) return source; const usingRe = /^using [^\n;]+;[ \t]*$/gm; let last: RegExpExecArray | null = null; let m: RegExpExecArray | null; while ((m = usingRe.exec(source)) !== null) last = m; if (!last) return source; const insertAt = last.index + last[0].length; return source.slice(0, insertAt) + `\nusing ${ns};` + source.slice(insertAt); } /** * One `services.AddSmartStackCodeKey<{Descriptor}>();` line per entity — * bi-generic `<{Descriptor}, {Probe}>` when the entry carries a `probeType` * (the hand-written ICodeUniquenessProbe the Suffix strategy requires). */ export function renderDiLines(spec: CodedEntitySpec): string { return spec.entities .map(e => { const probe = probeFqType(spec, e); const generics = probe ? `${descriptorFqType(spec, e)}, ${probe}` : descriptorFqType(spec, e); return `${DI_IND}services.AddSmartStackCodeKey<${generics}>();`; }) .join('\n'); } /** The content that lives BETWEEN the DI markers (markers included). */ export function renderDiBlock(spec: CodedEntitySpec): string { return `${DI_IND}${DI_BEGIN_MARKER}\n${renderDiLines(spec)}\n${DI_IND}${DI_END_MARKER}`; } /** Replace the whole BEGIN..END region (line-aligned) with `inner`. Returns null when unchanged. */ function replaceMarkerRegion(source: string, begin: string, end: string, inner: string): string | null { const beginIdx = source.indexOf(begin); const endIdx = source.indexOf(end); if (beginIdx < 0 || endIdx < 0 || endIdx <= beginIdx) return null; const lineStart = source.lastIndexOf('\n', beginIdx) + 1; const lineEnd = source.indexOf('\n', endIdx); const before = source.slice(0, lineStart); const after = lineEnd >= 0 ? source.slice(lineEnd + 1) : ''; const next = before + inner + (lineEnd >= 0 ? '\n' : '') + after; return next === source ? null : next; } /** * Idempotently patch the DI source. Returns the new source, or null when no * change was needed / no insertion point was found (caller warns). */ export function patchDi(source: string, spec: CodedEntitySpec): string | null { const withUsing = ensureUsing(source, CODE_GENERATION_DI_NAMESPACE); const replaced = replaceMarkerRegion(withUsing, DI_BEGIN_MARKER, DI_END_MARKER, renderDiBlock(spec)); if (replaced !== null) return replaced; if (withUsing.includes(DI_BEGIN_MARKER)) { return withUsing === source ? null : withUsing; } const full = [ `${DI_IND}// ── Coded entities (system-allocated business codes) ────────────────────`, `${DI_IND}// One ICodeKeyDescriptor per coded entity: the shared CodedEntitySaveHandler`, `${DI_IND}// allocates the Code atomically at insert (gapless), and the key appears in`, `${DI_IND}// the admin "Code patterns" screen. No seed, no migration — a CodePattern DB`, `${DI_IND}// row only ever overrides the descriptor. Requires the ExtensionsDbContext`, `${DI_IND}// ctor to forward IServiceProvider (shipped by the project template).`, renderDiBlock(spec), ].join('\n'); const returnRe = /\n[ \t]*return services;[ \t]*\n/g; let last: RegExpExecArray | null = null; let m: RegExpExecArray | null; while ((m = returnRe.exec(withUsing)) !== null) last = m; if (last) { return withUsing.slice(0, last.index) + '\n' + full + '\n' + withUsing.slice(last.index); } const lastBrace = withUsing.lastIndexOf('}'); if (lastBrace >= 0) return withUsing.slice(0, lastBrace) + full + '\n' + withUsing.slice(lastBrace); return null; }