upgrading/app/upgrades/0.13-to-0.14/migration-op-factories-to-methods.ts
upgrading/app/upgrades/0.13-to-0.14/migration-op-factories-to-methods.tsBrowse 76 files
2,854 tokens
10,243 bytes
Token encoding: o200k_base
Snapshot fac8604
← Back to SKILL.md
1/**2 * Removes bare migration op factory imports and rewrites call-sites to use3 * the method form on `this` (0.13 → 0.14):4 *5 * dropColumn(schema, table, col) → this.dropColumn({ schema, table, column: col })6 * setNotNull(schema, table, col) → this.setNotNull({ schema, table, column: col })7 * setDefault(schema, table, col, sql)8 * → this.setDefault({ schema, table, column: col, defaultSql: sql })9 * addPrimaryKey(schema, table, name, cols)10 * → this.addPrimaryKey({ schema, table, constraint: name, columns: cols })11 * addForeignKey(schema, table, { name, columns, references, onDelete })12 * → this.addForeignKey({ schema, table, foreignKey: { name, columns, references, onDelete } })13 * addCheckConstraint(schema, table, name, col, vals)14 * → this.addCheckConstraint({ schema, table, constraint: name, column: col, values: vals })15 * createIndex(schema, table, idx, cols)16 * → this.createIndex({ schema, table, index: idx, columns: cols })17 * installExtension({ ... }) → this.installExtension({ ... })18 *19 * Applies to files importing from '@internal/postgres/migration',20 * '@internal/target-postgres/migration', '@internal/sqlite/migration', or21 * '@internal/target-sqlite/migration'. Handles only call-sites where all22 * arguments are simple literals or identifiers on a single logical token (no23 * multi-line positional calls). For complex cases the type-checker will flag24 * remaining sites.25 *26 * Run from the project root:27 * pnpm exec tsx <path-to-this-file>28 */29 30import { execSync } from 'node:child_process';31import { readFileSync, writeFileSync } from 'node:fs';32import { join } from 'pathe';33 34const FACTORY_NAMES = [35 'dropColumn',36 'setNotNull',37 'setDefault',38 'addPrimaryKey',39 'addForeignKey',40 'addCheckConstraint',41 'createIndex',42 'installExtension',43];44 45/**46 * Strip bare factory names from import declarations (handles both single-line47 * and multi-line `import { ... } from '...'` forms). Removes the whole import48 * line (including trailing newline) when all names are factories.49 */50function stripFactoriesFromImports(src: string): string {51 const importRe =52 /^[^\S\n]*import\s*\{([^}]+)\}\s*from\s*'@internal\/(?:postgres|target-postgres|sqlite|target-sqlite)\/migration'[^\S\n]*;?[^\S\n]*\n?/gms;53 return src.replace(importRe, (full, nameBlock) => {54 const names = nameBlock55 .split(',')56 .map((n: string) => n.trim())57 .filter((n: string) => n.length > 0 && !FACTORY_NAMES.includes(n));58 if (names.length === 0) return '';59 const fromClause = full.slice(full.indexOf('}') + 1);60 return `import { ${names.join(', ')} }${fromClause}`;61 });62}63 64/** Reads a quoted string or bare identifier/bracket-balanced token starting at offset. */65function readToken(src: string, offset: number): { value: string; end: number } | null {66 let i = offset;67 while (i < src.length && src[i] === ' ') i++;68 if (i >= src.length) return null;69 if (src[i] === "'" || src[i] === '"' || src[i] === '`') {70 const q = src[i];71 let end = i + 1;72 while (end < src.length && src[end] !== q) end++;73 return { value: src.slice(i, end + 1), end: end + 1 };74 }75 let depth = 0;76 let end = i;77 while (end < src.length) {78 const c = src[end];79 if (c === '(' || c === '[' || c === '{') depth++;80 else if (c === ')' || c === ']' || c === '}') {81 if (depth === 0) break;82 depth--;83 } else if ((c === ',' || c === '\n') && depth === 0) break;84 end++;85 }86 return { value: src.slice(i, end).trim(), end };87}88 89type Rewrite = {90 pattern: RegExp;91 rewrite: (m: RegExpExecArray) => string | null;92};93 94const rewrites: Rewrite[] = [95 // dropColumn(schema, table, column)96 {97 pattern: /\bdropColumn\(/g,98 rewrite(m) {99 const rest = m.input.slice(m.index + m[0].length);100 const s = readToken(rest, 0);101 if (!s) return null;102 const t = readToken(rest, s.end + 1);103 if (!t) return null;104 const c = readToken(rest, t.end + 1);105 if (!c) return null;106 return `this.dropColumn({ schema: ${s.value}, table: ${t.value}, column: ${c.value} })`;107 },108 },109 // setNotNull(schema, table, column)110 {111 pattern: /\bsetNotNull\(/g,112 rewrite(m) {113 const rest = m.input.slice(m.index + m[0].length);114 const s = readToken(rest, 0);115 if (!s) return null;116 const t = readToken(rest, s.end + 1);117 if (!t) return null;118 const c = readToken(rest, t.end + 1);119 if (!c) return null;120 return `this.setNotNull({ schema: ${s.value}, table: ${t.value}, column: ${c.value} })`;121 },122 },123 // setDefault(schema, table, column, defaultSql)124 {125 pattern: /\bsetDefault\(/g,126 rewrite(m) {127 const rest = m.input.slice(m.index + m[0].length);128 const s = readToken(rest, 0);129 if (!s) return null;130 const t = readToken(rest, s.end + 1);131 if (!t) return null;132 const c = readToken(rest, t.end + 1);133 if (!c) return null;134 const d = readToken(rest, c.end + 1);135 if (!d) return null;136 return `this.setDefault({ schema: ${s.value}, table: ${t.value}, column: ${c.value}, defaultSql: ${d.value} })`;137 },138 },139 // addPrimaryKey(schema, table, constraintName, columns)140 {141 pattern: /\baddPrimaryKey\(/g,142 rewrite(m) {143 const rest = m.input.slice(m.index + m[0].length);144 const s = readToken(rest, 0);145 if (!s) return null;146 const t = readToken(rest, s.end + 1);147 if (!t) return null;148 const n = readToken(rest, t.end + 1);149 if (!n) return null;150 const c = readToken(rest, n.end + 1);151 if (!c) return null;152 return `this.addPrimaryKey({ schema: ${s.value}, table: ${t.value}, constraint: ${n.value}, columns: ${c.value} })`;153 },154 },155 // addCheckConstraint(schema, table, constraintName, column, values)156 {157 pattern: /\baddCheckConstraint\(/g,158 rewrite(m) {159 const rest = m.input.slice(m.index + m[0].length);160 const s = readToken(rest, 0);161 if (!s) return null;162 const t = readToken(rest, s.end + 1);163 if (!t) return null;164 const n = readToken(rest, t.end + 1);165 if (!n) return null;166 const c = readToken(rest, n.end + 1);167 if (!c) return null;168 const v = readToken(rest, c.end + 1);169 if (!v) return null;170 return `this.addCheckConstraint({ schema: ${s.value}, table: ${t.value}, constraint: ${n.value}, column: ${c.value}, values: ${v.value} })`;171 },172 },173 // createIndex(schema, table, indexName, columns)174 {175 pattern: /\bcreateIndex\(/g,176 rewrite(m) {177 const rest = m.input.slice(m.index + m[0].length);178 const s = readToken(rest, 0);179 if (!s) return null;180 const t = readToken(rest, s.end + 1);181 if (!t) return null;182 const idx = readToken(rest, t.end + 1);183 if (!idx) return null;184 const c = readToken(rest, idx.end + 1);185 if (!c) return null;186 return `this.createIndex({ schema: ${s.value}, table: ${t.value}, index: ${idx.value}, columns: ${c.value} })`;187 },188 },189 // addForeignKey(schema, table, { ... }) — wraps opts in `foreignKey:`190 {191 pattern: /\baddForeignKey\(/g,192 rewrite(m) {193 const rest = m.input.slice(m.index + m[0].length);194 const s = readToken(rest, 0);195 if (!s) return null;196 const t = readToken(rest, s.end + 1);197 if (!t) return null;198 const opts = readToken(rest, t.end + 1);199 if (!opts) return null;200 return `this.addForeignKey({ schema: ${s.value}, table: ${t.value}, foreignKey: ${opts.value} })`;201 },202 },203];204 205function applyRewrites(src: string): string {206 // installExtension already takes an object — just prepend `this.`207 let out = src.replace(/(?<!this\.)(?<!\.)\binstallExtension\(/g, 'this.installExtension(');208 209 for (const { pattern, rewrite } of rewrites) {210 pattern.lastIndex = 0;211 let result = '';212 let last = 0;213 let match = pattern.exec(out);214 while (match !== null) {215 const before = out.slice(Math.max(0, match.index - 5), match.index);216 if (before.endsWith('this.')) {217 result += out.slice(last, match.index + match[0].length);218 last = match.index + match[0].length;219 match = pattern.exec(out);220 continue;221 }222 const replacement = rewrite(match);223 if (replacement === null) {224 result += out.slice(last, match.index + match[0].length);225 last = match.index + match[0].length;226 match = pattern.exec(out);227 continue;228 }229 // Find the matching closing paren for the original call230 let depth = 1;231 let end = match.index + match[0].length;232 while (end < out.length && depth > 0) {233 if (out[end] === '(') depth++;234 else if (out[end] === ')') depth--;235 end++;236 }237 result += out.slice(last, match.index) + replacement;238 last = end;239 pattern.lastIndex = last;240 match = pattern.exec(out);241 }242 out = result + out.slice(last);243 }244 return out;245}246 247function processFile(src: string): string {248 const MIGRATION_IMPORT_RE =249 /import\s*\{[^}]+\}\s*from\s*'@internal\/(?:postgres|target-postgres|sqlite|target-sqlite)\/migration'/s;250 251 if (!MIGRATION_IMPORT_RE.test(src)) return src;252 253 const withImports = stripFactoriesFromImports(src);254 return applyRewrites(withImports);255}256 257const raw = execSync(258 'git ls-files --cached --others --exclude-standard -- "**migration.ts" "migration.ts"',259 { encoding: 'utf-8' },260).trim();261 262const files = raw263 .split('\n')264 .filter(Boolean)265 .filter((f) => f.endsWith('migration.ts'));266 267let changed = 0;268for (const file of files) {269 const abs = join(process.cwd(), file);270 let content: string;271 try {272 content = readFileSync(abs, 'utf-8');273 } catch {274 continue;275 }276 const relevant =277 content.includes("from '@internal/postgres/migration'") ||278 content.includes("from '@internal/target-postgres/migration'") ||279 content.includes("from '@internal/sqlite/migration'") ||280 content.includes("from '@internal/target-sqlite/migration'");281 if (!relevant) continue;282 283 const updated = processFile(content);284 if (updated !== content) {285 writeFileSync(abs, updated, 'utf-8');286 console.log(`updated ${file}`);287 changed++;288 }289}290console.log(`done — ${changed} file(s) updated`);291