/** * lib/dbcontext-markers — marker-block surgery on the C# seam hosts a client * project owns (ExtensionsDbContext.OnExtensionModelCreating, * DependencyInjection.cs). Extracted from scaffold-data-scope — the original * marker discipline — so every seam CLI (data scopes, tenant filters) patches * the hosts with the SAME primitives instead of re-implementing brace counting. * * Sibling: `lib/di-markers.ts` owns the DI-host flavour (DependencyInjection.cs * / Program.cs — `return services;` anchor); THIS lib owns the DbContext flavour * (OnExtensionModelCreating — ModelBuilder param + brace counting). Same marker * discipline, different structural anchors. * * Two patch styles compose from these primitives: * - FULL-SPEC region replacement (scaffold-data-scope): the caller renders the * whole BEGIN..END region from its spec each run → `replaceMarkerRegion`. * - PER-ENTITY line upsert (scaffold-entity tenant filters): each run knows * ONE entity only, so it inserts/replaces/removes its own line inside the * region and must not touch its siblings → `upsertMarkerLine`. */ /** 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) } /** Replace the whole BEGIN..END region (line-aligned) with `inner`. Returns null when unchanged/absent. */ export 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 } /** Extract the ModelBuilder parameter name of OnExtensionModelCreating (e.g. `modelBuilder`, `mb`). */ export function modelBuilderParam(source: string): string | null { const m = source.match(/OnExtensionModelCreating\s*\(\s*ModelBuilder\s+(\w+)\s*\)/) return m ? m[1] : null } /** * Locate the body of `OnExtensionModelCreating` and return the index of its * closing brace (brace-counted from the opening one). Null when not found. */ export function methodClosingBraceIndex(source: string): number | null { const sig = source.match(/OnExtensionModelCreating\s*\(\s*ModelBuilder\s+\w+\s*\)/) if (!sig || sig.index === undefined) return null const open = source.indexOf('{', sig.index + sig[0].length) if (open < 0) return null let depth = 0 for (let i = open; i < source.length; i++) { if (source[i] === '{') depth++ else if (source[i] === '}') { depth-- if (depth === 0) return i } } return null } /** Insert `block` (line-aligned) just before the closing brace of OnExtensionModelCreating. */ export function insertBlockBeforeMethodClose(source: string, block: string): string | null { const close = methodClosingBraceIndex(source) if (close === null) return null const lineStart = source.lastIndexOf('\n', close) + 1 return `${source.slice(0, lineStart)}\n${block}\n${source.slice(lineStart)}` } export interface MarkerLineUpsert { begin: string end: string /** Distinctive substring identifying THIS entry's line (e.g. ``). */ key: string /** Full statement line (indentation included), or null to REMOVE the entry. */ line: string | null } /** * Insert/replace/remove ONE line inside a BEGIN..END region, leaving every * sibling line untouched. Behaviour: * - the entry line is matched by `key` (substring); * - adding the first REAL line drops the placeholder `// …` example comments * the project template ships inside the region; * - real lines are kept SORTED so re-runs are order-independent. * Returns the new source, null when unchanged, or null when the markers are * absent (caller falls back to inserting a full block). */ export function upsertMarkerLine(source: string, u: MarkerLineUpsert): string | null { const beginIdx = source.indexOf(u.begin) const endIdx = source.indexOf(u.end) if (beginIdx < 0 || endIdx < 0 || endIdx <= beginIdx) return null const regionStart = source.indexOf('\n', beginIdx) + 1 if (regionStart === 0) return null // BEGIN marker on the last line — malformed const endLineStart = source.lastIndexOf('\n', endIdx) + 1 const region = source.slice(regionStart, endLineStart) const lines = region.length ? region.split('\n') : [] if (lines.length && lines[lines.length - 1] === '') lines.pop() const isPlaceholder = (l: string) => l.trimStart().startsWith('//') const real = lines.filter(l => !isPlaceholder(l) && l.trim() !== '') const placeholders = lines.filter(isPlaceholder) const kept = real.filter(l => !l.includes(u.key)) if (u.line !== null) kept.push(u.line) kept.sort() // Placeholders survive only while the region holds no real line. const nextLines = kept.length ? kept : placeholders const nextRegion = nextLines.length ? nextLines.join('\n') + '\n' : '' if (nextRegion === region) return null return source.slice(0, regionStart) + nextRegion + source.slice(endLineStart) }