upgrading/app/upgrades/0.11-to-0.12/re-emit-postgres-public-default.ts
upgrading/app/upgrades/0.11-to-0.12/re-emit-postgres-public-default.tsBrowse 76 files
1,604 tokens
6,378 bytes
Token encoding: o200k_base
Snapshot fac8604
← Back to SKILL.md
1/**2 * Re-emits every Postgres contract whose default namespace still uses the3 * pre-0.12 `__unbound__` / `postgres-unbound-schema` sentinel so emitted4 * `contract.json` / `contract.d.ts` pick up the `public` / `postgres-schema`5 * default (public-by-default).6 *7 * Starting at 0.12, un-namespaced Postgres models resolve to the `public`8 * namespace id. Explicit `namespace unbound { … }` in PSL still round-trips9 * to `__unbound__`; this script targets only contracts whose *default*10 * namespace is still the old sentinel shape.11 *12 * Dispatch: walks the project root for `prisma.config.ts` directories,13 * resolves each space's committed `contract.json`, and re-emits when the14 * storage tree still includes `"kind": "postgres-unbound-schema"`. Uses15 * the nearest ancestor `package.json` `scripts.emit` when present; otherwise16 * runs `prisma-next contract emit --config <path>`.17 *18 * Flags:19 * --check dry-run; lists contract-spaces that still need re-emitting and20 * exits 1 if any remain.21 */22import { execFile } from 'node:child_process';23import { access, readdir, readFile } from 'node:fs/promises';24import { dirname, join } from 'node:path';25import { promisify } from 'node:util';26 27const execFileAsync = promisify(execFile);28 29const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build']);30 31const dryRun = process.argv.includes('--check');32const projectRoot = process.cwd();33 34async function pathExists(path: string): Promise<boolean> {35 try {36 await access(path);37 return true;38 } catch {39 return false;40 }41}42 43function isJsonObject(value: unknown): value is Record<string, unknown> {44 return typeof value === 'object' && value !== null && !Array.isArray(value);45}46 47async function findPrismaNextConfigDirs(root: string): Promise<string[]> {48 const out: string[] = [];49 50 async function walk(dir: string): Promise<void> {51 let entries: Awaited<ReturnType<typeof readdir>>;52 try {53 entries = await readdir(dir, { withFileTypes: true });54 } catch {55 return;56 }57 for (const entry of entries) {58 if (entry.isDirectory()) {59 if (SKIP_DIRS.has(entry.name)) continue;60 await walk(join(dir, entry.name));61 } else if (entry.isFile() && entry.name === 'prisma.config.ts') {62 out.push(dir);63 }64 }65 }66 67 await walk(root);68 return out.sort();69}70 71function contractJsonCandidates(configDir: string): string[] {72 return [73 join(configDir, 'src', 'contract.json'),74 join(configDir, 'src', 'prisma', 'contract.json'),75 join(configDir, 'prisma', 'contract.json'),76 join(configDir, 'contract.json'),77 ];78}79 80async function resolveContractJson(configDir: string): Promise<string | null> {81 for (const candidate of contractJsonCandidates(configDir)) {82 if (await pathExists(candidate)) return candidate;83 }84 return null;85}86 87function contractNeedsPublicDefaultMigration(raw: string): boolean {88 return (89 raw.includes('"kind": "postgres-unbound-schema"') ||90 raw.includes('"kind":"postgres-unbound-schema"')91 );92}93 94async function packageJsonHasEmitScript(dir: string): Promise<boolean> {95 const pkgPath = join(dir, 'package.json');96 if (!(await pathExists(pkgPath))) return false;97 const raw = await readFile(pkgPath, 'utf-8');98 try {99 const parsed: unknown = JSON.parse(raw);100 if (!isJsonObject(parsed)) return false;101 const scripts = parsed['scripts'];102 if (!isJsonObject(scripts)) return false;103 return typeof scripts['emit'] === 'string' && scripts['emit'].length > 0;104 } catch {105 return false;106 }107}108 109async function packageJsonHasBuildContractSpaceScript(dir: string): Promise<boolean> {110 const pkgPath = join(dir, 'package.json');111 if (!(await pathExists(pkgPath))) return false;112 const raw = await readFile(pkgPath, 'utf-8');113 try {114 const parsed: unknown = JSON.parse(raw);115 if (!isJsonObject(parsed)) return false;116 const scripts = parsed['scripts'];117 if (!isJsonObject(scripts)) return false;118 return (119 typeof scripts['build:contract-space'] === 'string' &&120 scripts['build:contract-space'].length > 0121 );122 } catch {123 return false;124 }125}126 127async function resolveEmitInvocation(configDir: string): Promise<{128 readonly cwd: string;129 readonly args: string[];130 readonly key: string;131}> {132 let dir = configDir;133 while (dir.startsWith(projectRoot)) {134 if (await packageJsonHasEmitScript(dir)) {135 return { cwd: dir, args: ['emit'], key: `script:${dir}` };136 }137 const parent = dirname(dir);138 if (parent === dir) break;139 dir = parent;140 }141 const configPath = join(configDir, 'prisma.config.ts');142 return {143 cwd: projectRoot,144 args: ['exec', 'prisma-next', 'contract', 'emit', '--config', configPath],145 key: `config:${configPath}`,146 };147}148 149async function runEmit(configDir: string): Promise<void> {150 const { cwd, args } = await resolveEmitInvocation(configDir);151 await execFileAsync('pnpm', args, { cwd, env: process.env });152}153 154const configDirs = await findPrismaNextConfigDirs(projectRoot);155const emitKeys = new Set<string>();156const targets: Array<{ configDir: string; contractPath: string }> = [];157 158for (const configDir of configDirs) {159 if (await packageJsonHasBuildContractSpaceScript(configDir)) continue;160 const contractPath = await resolveContractJson(configDir);161 if (contractPath === null) continue;162 const raw = await readFile(contractPath, 'utf-8');163 if (!contractNeedsPublicDefaultMigration(raw)) continue;164 const { key } = await resolveEmitInvocation(configDir);165 if (emitKeys.has(key)) continue;166 emitKeys.add(key);167 targets.push({ configDir, contractPath });168}169 170if (targets.length === 0) {171 console.error(`No Postgres public-default migration candidates under ${projectRoot}.`);172 process.exit(dryRun ? 0 : 1);173}174 175let needsFix = 0;176 177for (const { configDir, contractPath } of targets) {178 const rel = configDir.slice(projectRoot.length + 1) || '.';179 const raw = await readFile(contractPath, 'utf-8');180 if (!contractNeedsPublicDefaultMigration(raw)) {181 console.log(`OK ${rel}`);182 continue;183 }184 needsFix += 1;185 if (dryRun) {186 console.log(`WOULD RE-EMIT ${rel}`);187 continue;188 }189 console.log(`EMIT ${rel}`);190 await runEmit(configDir);191}192 193console.log();194console.log(195 `${targets.length} contract-space(s): ${needsFix} ${dryRun ? 'needing re-emit' : 're-emitted'}.`,196);197 198if (dryRun && needsFix > 0) process.exit(1);199