kristiandupont / kanel

Generate Typescript types from Postgres
https://kristiandupont.github.io/kanel/
MIT License
858 stars 60 forks source link

kanel import name conflict when different schemas contain tables with the same name #589

Open aq1018 opened 1 month ago

aq1018 commented 1 month ago

I'm currently using multiple schemas as namespaces.

For example, I have the following database design:

The generated code looked like this:

import type { ProductId } from '../term_life/Product';
import type { ProductId } from '../income_protection/Product';

export default interface InsuranceApplicationTable {
  // ... other columns
  termLifeProductId: ColumnType<ProductId | null, ProductId | null, ProductId | null>;
  incomeProtectionProductId: ColumnType<ProductId | null, ProductId | null, ProductId | null>;
}

I made a preRenderHook to fix this, but it's a bit hacky:

function uniqueIdentifierImport(output) {
  for (const path in output) {
    const { declarations } = output[path]
    declarations.forEach((declaration) => {
      const { declarationType, properties } = declaration
      if (declarationType !== 'interface') return

      const importMap = {}

      // build import map with import name as the key,
      // and an array of (property idex, typeImport index) tuples as the value
      properties.forEach((property, propertyIndex) => {
        if (!property.typeImports) return

        property.typeImports.forEach((typeImport, typeImportIndex) => {
          importMap[typeImport.name] ??= []
          importMap[typeImport.name].push([propertyIndex, typeImportIndex])
        })
      })

      Object.values(importMap).forEach((locations) => {
        // skip if no duplicates
        if (locations.length < 2) return

        locations.forEach(([propertyIndex, typeImportIndex]) => {
          const property = properties[propertyIndex]
          const typeImport = property.typeImports[typeImportIndex]
          const pathSegments = typeImport.path.split('/')

          // extract schema name based on import path info
          // this is hacky, but I don't have a better way...
          const schema = pascalCase(pathSegments[pathSegments.length - 2])

          // update import name
          const oldImportName = typeImport.name
          const newImportName = `${schema}${oldImportName}`
          typeImport.name = `${oldImportName} as ${newImportName}`

          // update property type name
          property.typeName = property.typeName.replaceAll(
            oldImportName,
            newImportName,
          )
        })
      })
    })
  }
  return output
}

I'm not sure what the best solution entails, but the generated code should not create conflicting imports.

kristiandupont commented 1 month ago

Yeah, I expected that I would run into this sooner or later. The quick solution would probably be to support adding schema names as a prefix to model names, but I think few people would like that. The more correct solution would be that it intelligently detects the name clash and does something like

import type { ProductId as TermLifeProductId } from '../term_life/Product';
import type { ProductId as IncomeProtectionProductId } from '../income_protection/Product';

This should be possible but it's obviously not completely trivial. I will ponder this :-)