@@ -145,6 +145,60 @@ const META = {
145145 to : '1' . repeat ( 64 ) ,
146146} as const ;
147147
148+ const tscPath = join ( repoRoot , 'node_modules/.bin/tsc' ) ;
149+
150+ /**
151+ * Writes a rendered migration plus the tsconfig and contract-type fixtures
152+ * the typecheck tests need. The facade import is pointed at the live
153+ * workspace source (an absolute path specifier; bare workspace imports
154+ * inside the sources then resolve from their own package directories), so
155+ * tsc checks the rendered text against the real `createRlsPolicy`
156+ * signature. The execution fixtures' minimal `Contract` type does not
157+ * satisfy the `Migration` base's `Contract<SqlStorage>` constraint, so the
158+ * snapshot contract types come from the same dist types the migration
159+ * source graph resolves its own bare imports to.
160+ */
161+ async function writeTypecheckDir ( dir : string , renderedSource : string ) : Promise < void > {
162+ const tsSource = renderedSource . replace (
163+ "'@prisma-next/postgres/migration'" ,
164+ `'${ resolve ( targetPostgresRoot , 'src/exports/migration.ts' ) } '` ,
165+ ) ;
166+ await writeFile ( join ( dir , 'migration.ts' ) , tsSource ) ;
167+ const contractDistTypes = resolve (
168+ repoRoot ,
169+ 'packages/1-framework/0-foundation/contract/dist/types.mjs' ,
170+ ) ;
171+ const sqlContractDistTypes = resolve ( repoRoot , 'packages/2-sql/1-core/contract/dist/types.mjs' ) ;
172+ const realContractType = `export type Contract = import('${ contractDistTypes } ').Contract<\n import('${ sqlContractDistTypes } ').SqlStorage\n>;\n` ;
173+ for ( const hash of [ META . to , META . from ] ) {
174+ await writeFile (
175+ join ( dir , SNAPSHOTS_IMPORT_PATH , storageHashHex ( hash ) , 'contract.ts' ) ,
176+ realContractType ,
177+ ) ;
178+ }
179+ await writeFile (
180+ join ( dir , 'tsconfig.json' ) ,
181+ JSON . stringify ( {
182+ compilerOptions : {
183+ target : 'ES2022' ,
184+ module : 'preserve' ,
185+ moduleResolution : 'bundler' ,
186+ lib : [ 'ES2022' ] ,
187+ strict : true ,
188+ exactOptionalPropertyTypes : true ,
189+ noUncheckedIndexedAccess : true ,
190+ skipLibCheck : true ,
191+ noEmit : true ,
192+ allowImportingTsExtensions : true ,
193+ resolveJsonModule : true ,
194+ typeRoots : [ join ( repoRoot , 'node_modules/@types' ) ] ,
195+ types : [ 'node' ] ,
196+ } ,
197+ include : [ 'migration.ts' ] ,
198+ } ) ,
199+ ) ;
200+ }
201+
148202describe ( 'TypeScriptRenderablePostgresMigration round-trip' , ( ) => {
149203 let tmpDir : string ;
150204
@@ -302,60 +356,55 @@ describe('TypeScriptRenderablePostgresMigration round-trip', () => {
302356 testAdapter ,
303357 ) ;
304358
305- // Point the facade import at the live workspace source (an absolute path
306- // specifier; bare workspace imports inside the sources then resolve from
307- // their own package directories) so tsc checks the rendered text against
308- // the real `createRlsPolicy` signature.
309- const tsSource = migration
310- . renderTypeScript ( )
311- . replace (
312- "'@prisma-next/postgres/migration'" ,
313- `'${ resolve ( targetPostgresRoot , 'src/exports/migration.ts' ) } '` ,
314- ) ;
315- await writeFile ( join ( tmpDir , 'migration.ts' ) , tsSource ) ;
316- // The execution fixtures' minimal `Contract` type does not satisfy the
317- // `Migration` base's `Contract<SqlStorage>` constraint; the typecheck
318- // needs the real one, taken from the same dist types the migration
319- // source graph resolves its own bare imports to.
320- const contractDistTypes = resolve (
321- repoRoot ,
322- 'packages/1-framework/0-foundation/contract/dist/types.mjs' ,
323- ) ;
324- const sqlContractDistTypes = resolve ( repoRoot , 'packages/2-sql/1-core/contract/dist/types.mjs' ) ;
325- const realContractType = `export type Contract = import('${ contractDistTypes } ').Contract<\n import('${ sqlContractDistTypes } ').SqlStorage\n>;\n` ;
326- for ( const hash of [ META . to , META . from ] ) {
327- await writeFile (
328- join ( tmpDir , SNAPSHOTS_IMPORT_PATH , storageHashHex ( hash ) , 'contract.ts' ) ,
329- realContractType ,
330- ) ;
331- }
332- await writeFile (
333- join ( tmpDir , 'tsconfig.json' ) ,
334- JSON . stringify ( {
335- compilerOptions : {
336- target : 'ES2022' ,
337- module : 'preserve' ,
338- moduleResolution : 'bundler' ,
339- lib : [ 'ES2022' ] ,
340- strict : true ,
341- exactOptionalPropertyTypes : true ,
342- noUncheckedIndexedAccess : true ,
343- skipLibCheck : true ,
344- noEmit : true ,
345- allowImportingTsExtensions : true ,
346- resolveJsonModule : true ,
347- typeRoots : [ join ( repoRoot , 'node_modules/@types' ) ] ,
348- types : [ 'node' ] ,
349- } ,
350- include : [ 'migration.ts' ] ,
351- } ) ,
352- ) ;
353-
354- const tscPath = join ( repoRoot , 'node_modules/.bin/tsc' ) ;
359+ await writeTypecheckDir ( tmpDir , migration . renderTypeScript ( ) ) ;
355360 // Non-zero exit (a type error in the rendered source) rejects.
356361 await execFileAsync ( tscPath , [ '--project' , tmpDir ] ) ;
357362 } ) ;
358363
364+ it ( 'the typecheck catches a rendered literal missing a required key (negative control)' , {
365+ timeout : timeouts . typeScriptCompilation ,
366+ } , async ( ) => {
367+ const migration = new TypeScriptRenderablePostgresMigration (
368+ [
369+ new CreatePostgresRlsPolicyCall (
370+ 'public' ,
371+ 'user' ,
372+ new PostgresRlsPolicy ( {
373+ name : 'Tenant members can read' ,
374+ prefix : undefined ,
375+ tableName : 'user' ,
376+ namespaceId : 'public' ,
377+ operation : 'select' ,
378+ roles : [ 'app_user' ] ,
379+ using : '(tenant_id = 1)' ,
380+ withCheck : undefined ,
381+ permissive : true ,
382+ } ) ,
383+ ) ,
384+ ] ,
385+ META ,
386+ APP_SPACE_ID ,
387+ SNAPSHOTS_IMPORT_PATH ,
388+ testAdapter ,
389+ ) ;
390+
391+ // Delete the policy's required `name` key from the rendered source — the
392+ // compile must fail (TS2741 missing-property), proving the green run of
393+ // the sibling test is a real typecheck and not a vacuous pass.
394+ const brokenSource = migration
395+ . renderTypeScript ( )
396+ . replace ( ' name: "Tenant members can read",\n' , '' ) ;
397+ expect ( brokenSource ) . not . toContain ( 'Tenant members can read' ) ;
398+ await writeTypecheckDir ( tmpDir , brokenSource ) ;
399+
400+ const failure = await execFileAsync ( tscPath , [ '--project' , tmpDir ] ) . then (
401+ ( ) => undefined ,
402+ ( error : unknown ) => error ,
403+ ) ;
404+ expect ( failure ) . toBeDefined ( ) ;
405+ expect ( String ( ( failure as { stdout ?: string } ) . stdout ) ) . toContain ( 'TS2741' ) ;
406+ } ) ;
407+
359408 it ( 'preserves RawSqlCall ops byte-for-byte through the render → execute round-trip' , {
360409 timeout : timeouts . typeScriptCompilation ,
361410 } , async ( ) => {
0 commit comments