Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
aq1018 opened this issue Jul 18, 2024 · 2 comments

Comments

@aq1018
Copy link

aq1018 commented Jul 18, 2024

I'm currently using multiple schemas as namespaces.

For example, I have the following database design:

  • In term_life schema, I have a product table.
  • In income_protection schema, I have a product table as well, but with different columns.
  • In public schema, I have a insurance_application table that contains foreign keys referencing both products.
    • term_life_product_id referencing term_life.product.id.
    • income_protection_product_id referencing income_protection.prodcut.id
    • Only one of them can be populated.

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
Copy link
Owner

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 :-)

@alita-moore
Copy link

here's my workaround thanks to the awesome postRenderHook config and gpt

const path = require("path")

/**
 * Post-render hook to resolve duplicate imports by renaming conflicting identifiers with schema-based aliases.
 * @param {string} filePath - The file path.
 * @param {string[]} lines - The content lines of the file.
 * @param {object} instantiatedConfig - The instantiated configuration object.
 * @returns {Promise<string[]>} - The modified lines with resolved naming conflicts.
 */
async function resolveDuplicateImports(filePath, lines, instantiatedConfig) {
  // Collect import statements
  const imports = [];
  const importRegex = /^import type { (\w+) } from ['"](.+)['"];$/;
  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
    const match = line.match(importRegex);
    if (match) {
      const [_, importedIdentifier, modulePath] = match;
      imports.push({ lineIndex: i, importedIdentifier, modulePath });
    }
  }

  // Build a mapping of imported identifiers to their module paths
  const importMap = {};
  for (const imp of imports) {
    const { importedIdentifier, modulePath } = imp;
    if (!importMap[importedIdentifier]) {
      importMap[importedIdentifier] = [];
    }
    importMap[importedIdentifier].push(modulePath);
  }

  // Identify duplicate imports
  const duplicates = {};
  for (const importedIdentifier in importMap) {
    const modulePaths = importMap[importedIdentifier];
    if (modulePaths.length > 1) {
      duplicates[importedIdentifier] = modulePaths;
    }
  }

  if (Object.keys(duplicates).length === 0) {
    // No duplicates, return lines as is
    return lines;
  }

  // Generate aliases for duplicate imports
  const aliases = {}; // key: importedIdentifier + '|' + modulePath, value: alias
  const schemaToAlias = {}; // key: importedIdentifier, value: { schemaName: alias }

  for (const importedIdentifier in duplicates) {
    schemaToAlias[importedIdentifier] = {};
    const modulePaths = duplicates[importedIdentifier];
    for (const modulePath of modulePaths) {
      // Resolve the absolute module path
      const absoluteModulePath = path.resolve(path.dirname(filePath), modulePath);
      // Extract the schema name
      const schemaName = getSchemaNameFromModulePath(absoluteModulePath);
      // Generate alias
      const alias = importedIdentifier + capitalize(schemaName);
      aliases[importedIdentifier + '|' + modulePath] = alias;
      schemaToAlias[importedIdentifier][schemaName] = alias;
    }
  }

  // Update import statements to include aliases
  for (const imp of imports) {
    const { lineIndex, importedIdentifier, modulePath } = imp;
    if (duplicates[importedIdentifier]) {
      const alias = aliases[importedIdentifier + '|' + modulePath];
      lines[lineIndex] = `import type { ${importedIdentifier} as ${alias} } from '${modulePath}';`;
    }
  }

  // Function to capitalize the first letter
  function capitalize(str) {
    if (!str) return str;
    return str.charAt(0).toUpperCase() + str.slice(1);
  }

  // Function to get schema name from module path
  function getSchemaNameFromModulePath(absoluteModulePath) {
    const pathSegments = absoluteModulePath.split(path.sep);
    const modelsIndex = pathSegments.lastIndexOf('models');
    if (modelsIndex >= 0 && modelsIndex + 1 < pathSegments.length) {
      const schemaName = pathSegments[modelsIndex + 1];
      return schemaName;
    }
    return null;
  }

  // Function to get schema name from property name
  function getSchemaNameFromPropertyName(propertyName) {
    const prefix = propertyName.split('_')[0];
    return prefix;
  }

  // Update the rest of the code to use aliases
  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
    // Skip import lines
    if (line.startsWith('import')) continue;

    // Match property definitions
    const propertyMatch = line.match(/^\s*(\w+):\s*ColumnType<(.+)>;$/);
    if (propertyMatch) {
      const propertyName = propertyMatch[1];
      const columnTypeContent = propertyMatch[2];
      const types = columnTypeContent.split(',').map(t => t.trim());
      const updatedTypes = types.map(type => {
        if (duplicates[type]) {
          // Determine schema name from property name
          const schemaName = getSchemaNameFromPropertyName(propertyName);
          const alias = schemaToAlias[type][schemaName];
          if (alias) {
            return alias;
          } else {
            // Could not find alias for schema, default to original type
            return type;
          }
        } else {
          return type;
        }
      });
      // Reconstruct the line
      const updatedColumnTypeContent = updatedTypes.join(', ');
      lines[i] = `  ${propertyName}: ColumnType<${updatedColumnTypeContent}>;`;
    }
  }

  return lines;
}


module.exports = {
  importsExtension: ".js",
  postRenderHooks: [resolveDuplicateImports],
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants