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

Generating iterable union types with kanel-kysely #567

Open
ersinakinci opened this issue Jun 8, 2024 · 6 comments
Open

Generating iterable union types with kanel-kysely #567

ersinakinci opened this issue Jun 8, 2024 · 6 comments

Comments

@ersinakinci
Copy link

ersinakinci commented Jun 8, 2024

I've switched from using enums to iterable type unions, as outlined in this post. I find them much more ergonomic than traditional TS enums and they have the benefit of being iterable.

Any chance that we could get an option in the kanel-kysely plugin to generate this kind of union for PostgreSQL enums instead of native TS enums when using a custom type? For example, instead of generating:

enum EntityType {
  person = 'person',
  company = 'company',
};

export default EntityType

We could generate:

export const EntityType = [
  "person",
  "company",
] as const;

export type EntityType = (typeof EntityType)[number];

Opt-in API:

// .kanelrc.js

export default {
   ...,
   preRenderHooks: [makeKyselyHook({ iterableUnionTypes: true })],
}
@ersinakinci
Copy link
Author

ersinakinci commented Jun 8, 2024

Searching through the code, I discovered the enumStyle option:

// invokeKanelProgrammatically.js

import kanel from "kanel";
import config from "../.kanelrc";

const { processDatabase } = kanel;

async function run() {
  await processDatabase({ ...config, enumStyle: "type" });
}

run();

For some reason, I can't use it in my .kanelrc.js file, I have to use it in a programmatically-invoked Kanel config.

The result is:

type EntityType = 
  | 'person'
  | 'company';

export default EntityType;

Which gets us really close!

Could we just simply add a readonly array with the enum types as strings and export that as a named export when using enumStyle: "type"? And also export the type as a named export, the same way that I indicated in my original post? (No need to change the default export.)

And could we also allow setting enumStyle from within .kanelrc.js?

@kristiandupont
Copy link
Owner

And could we also allow setting enumStyle from within .kanelrc.js?

I thought it was, that's strange!

But this is a good point. I was actually contemplating removing the enum option altogether because I don't use it myself but I acknowledge that others do and I don't want to cause too many breaking changes. The thing that annoys me the most, which would apply to your suggestion as well, is that it's the only place where Kanel emits runable code (as opposed to just types). But as it's like that already, your suggestion doesn't make this any worse :-D

I guess this could be a third enum style. I would also want to rename the type type to union as I think that's more intuitive.

@ersinakinci
Copy link
Author

I thought it was, that's strange!

Maybe you can set it from the config file? I just know that it doesn't show up in IntelliSense 😛, so at least the types are missing.

I guess this could be a third enum style. I would also want to rename the type type to union as I think that's more intuitive.

Works for me! That would be great. I hope it doesn't go against your design goals too much 😅.

@acro5piano
Copy link

It's really good if the feature is natively implemented!

Current my workaround hook is here:

const extractEnumValuesHook = (path, lines) => {
  let isEnumFile = lines.some((line) => line.includes('Represents the enum'))
  if (!isEnumFile) {
    return lines
  }
  const l = lines.length
  for (let i = 0; i < l; i++) {
    {
      const match = lines[i].match(/export type (.+) =/)
      if (match) {
        lines.push(`export const ${match[1]}Values = [`)
      }
    }
    {
      const match = lines[i].match(/\| '(.+)'/)
      if (match) {
        lines.push(`  '${match[1]}', `)
      }
    }
  }
  lines.push('] as const')
  return lines
}

module.exports = {
  connection: process.env['DATABASE_URL'],
  preRenderHooks: [makeKyselyHook(), kyselyCamelCaseHook],
  postRenderHooks: [
    extractEnumValuesHook,
  ],
  enumStyle: 'type',
}

Outputs this:

/** Represents the enum public.gender_enum */
export type GenderEnum = 
  | 'MALE'
  | 'FEMALE'
  | 'UNKNOWN';


export const GenderEnumValues = [
  'MALE', 
  'FEMALE', 
  'UNKNOWN', 
] as const

@ersinakinci
Copy link
Author

ersinakinci commented Aug 20, 2024

@acro5piano's hook didn't work for me. I'm not sure about his setup, but it looks to me like the type files are exported using export default, which the code above doesn't detect.

I expanded on @acro5piano's hook:

  • Detects export default and removes it.
  • Exports two named exports, one type and one iterable union constant, with the same name (e.g., a const and a type both named MyType and both exported as named exports; rather than a named export const named MyTypeValues and a default export type named MyType). That's just my personal preference.
  • Rewrites the table schema files to import the named exports rather than the default exports.
  • Has TS typing.
// .kanelrc.ts

const extractEnumValuesHook = (_path: string, lines: string[]) => {
  let l = lines.length;
  const isTableFile = lines.some((line: string) =>
    line.includes("Represents the table")
  );
  const isEnumFile = lines.some((line: string) =>
    line.includes("Represents the enum")
  );

  if (isTableFile) {
    for (let i = 0; i < l; i++) {
      const match = lines[i].match(/^import type { default as (.+) }/);
      if (match) {
        lines[i] = `import type { ${match[1]} } from './${match[1]}';`;
      }
    }
  }

  if (isEnumFile) {
    for (let i = 0; i < l; i++) {
      {
        const match = lines[i].match(/^type (.+) =/);
        if (match) {
          lines[i] = `export const ${match[1]} = [`;
          lines.push(`export type ${match[1]} = (typeof ${match[1]})[number];`);
        }
      }
      {
        const match = lines[i].match(/^export default/);
        if (match) {
          lines.splice(i, 1);
          l--;
        }
      }
      {
        const match = lines[i].match(/\| '(.+)'$/);
        if (match) {
          lines[i] = `  '${match[1]}', `;
        }
      }
      {
        const match = lines[i].match(/\| '(.+)';$/);
        if (match) {
          lines[i] = `  '${match[1]}',`;
          lines.splice(i + 1, 0, `] as const;`);
          l++;
        }
      }
    }
  }

  return lines;
};

// Kanel config
export default {
  ...,
  postRenderHooks: [extractEnumValuesHook],
  enumStyle: "type",
};

Example output:

EntityType.ts (enum file)

/** Represents the enum public.entity_type */
export const EntityType = [
  'person', 
  'sole-proprietorship', 
  'gp', 
  'lp', 
  'corporation', 
  'llc', 
  'llp', 
  'cooperative', 
  'other',
] as const;

export type EntityType = (typeof EntityType)[number];

Entity.ts (table file)

import type { EntityType } from './EntityType';

/** Represents the table public.entity */
export default interface EntityTable {
  type: ColumnType<EntityType, EntityType, EntityType>;
  
  ...
}

Would be great to have this built into Kanel 😄

@kristiandupont
Copy link
Owner

Nice work! I'm happy you got it working.

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