Support Knex enums (CHECK constraints)
AudunWA opened this issue ยท 4 comments
When building tables using the enu
function in Knex, the default behaviour is to add a CHECK constraint on the column instead of creating a pg enum. Example:
await knex.schema.table("foo", (tableBuilder) => {
tableBuilder.enu("bar", ["a", "b", "c"]).defaultTo("a").notNullable();
});
create table public.foo
(
bar text default 'a'::text not null
constraint foo_bar_check
check (extracted_by = ANY (ARRAY ['a'::text, 'b'::text, 'c'::text]))
);
Ideally, kanel should be able to generate
interface Foo {
bar: "a" | "b" | "c";
}
from this table definition.
Aha, interesting. This would have to rely on a heuristic though, which isn't really how anything works in Kanel (at the moment). I think I could enable it as a hook though, I'll look into that!
@AudunWA added some basic check support
kristiandupont/extract-pg-schema#472
The problem with CHECK clause syntax is that it's extracted "as is" and you have to parse it yourself.
I'm planning to contribute to kanel-zod and implement kanel-effect-schema codegen out of CHECK constraints and column comments.
@kristiandupont prefers ramda, and I do prefer fp-ts with parser-ts. Kristian could you please clarify what parser-combinator PEG-parser would be preferable to parse the CHECK constraints ? I'd, personally, go for parzec, but it doesn't really matter for me.
@yuriy-yarosh I used peggy for tagged-comment-parser, but that's not a requirement of any kind.
I want to stress that anyone can create "extensions" for Kanel and just publish them on NPM themselves. I am happy to link to them from the documentation. If you want it to be a part of this repository, I would like to retain some level of consistency but if you don't care for Ramda (I personally have lost some affection for it because of the incomplete types), you don't have to use it ๐
With @yuriy-yarosh's changes in kristiandupont/extract-pg-schema#472, I was able to generate the types I wanted (although a bit quick and dirty, but not an issue in my case).
Example implementation:
getPropertyMetadata: (property, _details, generateFor) => {
return {
typeOverride: getTypeFromCheck(
_details.name,
property.name,
_details.checks
),
};
}
function getTypeFromCheck(tableName, columnName, checks) {
const check = checks.find(
(check) =>
check.clause.startsWith(`${columnName} = ANY (ARRAY[`) ||
check.clause.startsWith(`${columnName} = '`)
);
if (check == null) {
return undefined;
}
if (check.clause.startsWith(`${columnName} = ANY (ARRAY[`)) {
const values = check.clause
.match(/ARRAY\[(.*)]/)[1]
.split(",")
.map((it) =>
it.trim().replaceAll("'", "").replaceAll("::text", "")
);
return values.map((it) => `"${it}"`).join(" | ");
} else if (check.clause.startsWith(`${columnName} = '`)) {
const value = check.clause.match(/'(.*)'::text/)[1];
return `"${value}"`;
} else {
return undefined;
}
}