Skip to content

Instantly share code, notes, and snippets.

@virtuallyunknown
Last active July 20, 2023 12:00
Show Gist options
  • Save virtuallyunknown/8168802b352897a263b55b12287d4a25 to your computer and use it in GitHub Desktop.
Save virtuallyunknown/8168802b352897a263b55b12287d4a25 to your computer and use it in GitHub Desktop.
Kanel + Kysely
import { join, relative, sep } from "node:path";
import { recase } from '@kristiandupont/recase';
import kanel from 'kanel';
import kanelKysely from 'kanel-kysely';
const toCamelCase = recase('snake', 'camel');
const toPascalCase = recase('snake', 'pascal');
export function trimWhitespaceHook(path, lines, instantiatedConfig) {
return lines.filter((line, index, array) => {
if (line === '' && array[index - 1].startsWith(' ')) {
return;
}
return line === '' ? '\n' : line;
});
}
export function convertESMPathsHook(path, lines, instantiatedConfig) {
return lines.map(line => line.replace(/^import\stype\s.*'(.*)';$/, (match, p1) => {
return /\sfrom\s'kysely';$/.test(match)
? match
: match.replace(p1, `${p1}.js`)
}))
}
function getKyselyItemMetadata(details, selectorName, canInitialize, canMutate) {
const typeNames = generateTypeNames({ name: details.name, type: 'table' })
return ({
tableInterfaceName: typeNames.tableInterfaceName,
selectableName: typeNames.selectableName,
insertableName: canInitialize ? typeNames.insertableName : undefined,
updatableName: canMutate ? typeNames.updatableName : undefined,
})
}
function generateTypeNames({ name, type = null } = {}) {
const pascalName = toPascalCase(name);
if (type === 'table') {
return {
tableInterfaceName: `DB${pascalName}`,
selectableName: `DB${pascalName}Selectable`,
insertableName: `DB${pascalName}Insertable`,
updatableName: `DB${pascalName}Updateable`
}
}
else if (type === 'identifier') {
return {
identifierName: `DB${pascalName}`
}
}
}
function generateIndexHook(outputAcc, instantiatedConfig) {
const allEntities = Object.values(instantiatedConfig.schemas).reduce((acc, elem) => {
const entitiesInSchema = Object.values(elem)
.filter(Array.isArray)
.reduce((acc2, elem2) => [...acc2, ...elem2], []);
return [...acc, ...entitiesInSchema];
}, []);
const lines = allEntities.map((details) => {
let result;
const { path } = instantiatedConfig.getMetadata(details, "selector", instantiatedConfig);
let importPath = relative(instantiatedConfig.outputPath, path);
if (sep === "\\") {
importPath = importPath.replace(/\\/g, "/");
}
if (details.kind === "table") {
const { tableInterfaceName, selectableName, insertableName, updatableName } = generateTypeNames({ name: details.name, type: 'table' });
const additionalImports = [selectableName, insertableName, updatableName];
if (instantiatedConfig.generateIdentifierType) {
const identifierColumns = details.columns.filter((column) => column.isPrimaryKey && !column.reference);
identifierColumns.forEach((column) => {
const { identifierName } = generateTypeNames({ name: `${details.name}_${column.name}`, type: 'identifier' });
additionalImports.push(identifierName);
});
}
result = `export type { default as ${tableInterfaceName}, ${additionalImports.join(", ")} } from './${importPath}.js';`;
}
else if (details.kind === 'view' || details.kind === 'materializedView') {
const { identifierName } = generateTypeNames({ name: details.name, type: 'identifier' });
result = `export type { default as ${identifierName} } from './${importPath}.js';`;
}
else if (details.kind === "enum") {
const { identifierName } = generateTypeNames({ name: details.name, type: 'identifier' });
const prefix = instantiatedConfig.enumStyle === "type" ? "type " : "";
result = `export ${prefix}{ default as ${identifierName} } from './${importPath}.js';`;
}
else {
console.log(details.kind);
const { name } = instantiatedConfig.getMetadata(details, "selector", instantiatedConfig);
result = `export type { default as ${name} } from './${importPath}.js';`;
}
return result;
});
const indexFile = {
declarations: [
{
declarationType: "generic",
lines,
},
],
};
const path = join(instantiatedConfig.outputPath, "db-types");
return {
...outputAcc,
[path]: indexFile,
};
}
async function processDatabase() {
await kanel.processDatabase({
connection: {
// credentials
},
outputPath: 'src/types',
resolveViews: true,
preDeleteOutputFolder: true,
enumStyle: 'type',
preRenderHooks: [kanelKysely.makeKyselyHook(({ databaseFilename: 'db', getKyselyItemMetadata })), generateIndexHook],
postRenderHooks: [trimWhitespaceHook, convertESMPathsHook],
getMetadata: (details, generateFor, instantiatedConfig) => {
const suffix = ['selector', 'initializer', 'mutator'].includes(generateFor) && details.kind !== 'enum'
? `_${generateFor}`
: '';
return {
name: toPascalCase(`${details.name}${suffix}`),
comment: [`generateFor: ${generateFor} | details.name: ${details.name} | details.kind: ${details.kind}`],
path: join(instantiatedConfig.outputPath, 'db', toPascalCase(details.name)),
};
},
getPropertyMetadata: (property, details, generateFor) => {
return {
name: toCamelCase(property.name),
}
},
generateIdentifierType: (column, details, config) => {
const { identifierName } = generateTypeNames({ name: `${details.name}_${column.name}`, type: 'identifier' })
const innerType = kanel.resolveType(column, details, {
...config,
generateIdentifierType: undefined,
});
return {
declarationType: 'typeDeclaration',
name: identifierName,
exportAs: 'named',
typeDefinition: [innerType],
}
}
});
}
await processDatabase();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment