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

kysely schemas are not separated #620

Open
alita-moore opened this issue Nov 17, 2024 · 4 comments
Open

kysely schemas are not separated #620

alita-moore opened this issue Nov 17, 2024 · 4 comments

Comments

@alita-moore
Copy link

alita-moore commented Nov 17, 2024

it seems that kanel-kysely normally merges schemas together in the Database type which make it difficult to know which table is from which schema. How can I adjust my config so that this

// @generated
// This file is automatically generated by Kanel. Do not modify manually.

import type { default as ObjectsTable } from './Objects.js';

export default interface QueueSchema {
 objects: ObjectsTable;
}

is turned into

// @generated
// This file is automatically generated by Kanel. Do not modify manually.

import type { default as ObjectsTable } from './Objects.js';

export default interface QueueSchema {
  "queue.objects": ObjectsTable;
}

I'm getting this updated type based off of kysely's instructions on how to deal with schemas: https://kysely.dev/docs/recipes/schemas

@alita-moore alita-moore changed the title kysely schemas kysely schemas are not separated Nov 17, 2024
@alita-moore
Copy link
Author

this seems to address the problem:

const { makeKyselyHook } = require("kanel-kysely");

/**
 * Post-render hook to prefix property names with their schema in the Schema files.
 * @param {string} path - 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 schema-prefixed property names.
 */
async function prefixTableNamesWithSchema(path, lines, instantiatedConfig) {
  // Check if the current file ends with 'Schema.ts'
  if (!path.endsWith('Schema.ts')) {
    return lines;
  }

  // Extract the schema name from the path
  const pathSegments = path.split('/');
  const schemaName = pathSegments[pathSegments.length - 2];

  // Regular expression to match property entries inside interfaces
  const propertyEntryRegex = /^(\s*)(\w+): (\w+);$/;

  // Map through each line and modify property entries
  return lines.map((line) => {
    const match = line.match(propertyEntryRegex);
    if (match) {
      const [_, indentation, propertyName, propertyType] = match;
      const newPropertyName = `'${schemaName}.${propertyName}'`;
      const newLine = `${indentation}${newPropertyName}: ${propertyType};`;
      return newLine;
    }
    return line;
  });
}

module.exports = {
  importsExtension: ".js",
  preRenderHooks: [makeKyselyHook()],
  postRenderHooks: [prefixTableNamesWithSchema],
};

@alita-moore
Copy link
Author

alita-moore commented Nov 17, 2024

just for reference here's my final config which also addresses naming conflicts between schemas as discussed in this issue: #589

const { makeKyselyHook } = require("kanel-kysely");
const path = require("path");

async function prefixTableNamesWithSchema(path, lines, instantiatedConfig) {
  if (!path.endsWith("Schema.ts")) {
    return lines;
  }

  const pathSegments = path.split("/");
  const schemaName = pathSegments[pathSegments.length - 2];

  const propertyEntryRegex = /^(\s*)(\w+): (\w+);$/;

  return lines.map(line => {
    const match = line.match(propertyEntryRegex);
    if (match) {
      const [_, indentation, propertyName, propertyType] = match;
      const newPropertyName = `'${schemaName}.${propertyName}'`;
      const newLine = `${indentation}${newPropertyName}: ${propertyType};`;
      return newLine;
    }
    return line;
  });
}

async function resolveDuplicateImports(filePath, lines, instantiatedConfig) {
  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 });
    }
  }

  const importMap = {};
  for (const imp of imports) {
    const { importedIdentifier, modulePath } = imp;
    if (!importMap[importedIdentifier]) {
      importMap[importedIdentifier] = [];
    }
    importMap[importedIdentifier].push(modulePath);
  }

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

  if (Object.keys(duplicates).length === 0) {
    return lines;
  }

  const aliases = {};
  const schemaToAlias = {};

  for (const importedIdentifier in duplicates) {
    schemaToAlias[importedIdentifier] = {};
    const modulePaths = duplicates[importedIdentifier];
    for (const modulePath of modulePaths) {
      const absoluteModulePath = path.resolve(
        path.dirname(filePath),
        modulePath
      );
      const schemaName = getSchemaNameFromModulePath(absoluteModulePath);
      const alias = importedIdentifier + capitalize(schemaName);
      aliases[importedIdentifier + "|" + modulePath] = alias;
      schemaToAlias[importedIdentifier][schemaName] = alias;
    }
  }

  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 capitalize(str) {
    if (!str) return str;
    return str.charAt(0).toUpperCase() + str.slice(1);
  }

  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 getSchemaNameFromPropertyName(propertyName) {
    const prefix = propertyName.split("_")[0];
    return prefix;
  }

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
    if (line.startsWith("import")) continue;

    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]) {
          const schemaName = getSchemaNameFromPropertyName(propertyName);
          const alias = schemaToAlias[type][schemaName];
          if (alias) {
            return alias;
          } else {
            return type;
          }
        } else {
          return type;
        }
      });
      const updatedColumnTypeContent = updatedTypes.join(", ");
      lines[i] = `  ${propertyName}: ColumnType<${updatedColumnTypeContent}>;`;
    }
  }

  return lines;
}

module.exports = {
  importsExtension: ".js",
  preRenderHooks: [makeKyselyHook()],
  postRenderHooks: [prefixTableNamesWithSchema, resolveDuplicateImports],
};

@kristiandupont
Copy link
Owner

Awesome! I think this is the most complex custom hook I've seen here so far :-)

@alita-moore
Copy link
Author

alita-moore commented Nov 19, 2024

yeah.. it's pretty hacky, but it gets the job done :)

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

2 participants