UNPKG

okai

Version:

AI-powered code generation tool for ServiceStack Apps. Generate TypeScript data models, C# APIs, migrations, and UI components from natural language prompts using LLMs.

334 lines 12.9 kB
import { pick, splitCase, toCamelCase, toPascalCase } from "./utils.js"; // Transforms that are only applied once on AI TypeScript AST export function createTdAstFromAIAst(tsAst, groupName) { rewriteInterfaceNames(tsAst); mergeInterfacesAndClasses(tsAst); rewriteToPascalCase(tsAst); replaceReferences(tsAst); replaceIds(tsAst); replaceConflictTypes(tsAst); tsAst.classes.forEach(cls => { // replaceUserRefs(cls) replaceUserBaseClass(cls); rewriteDuplicateTypePropNames(cls); removeDuplicateProps(cls); rewriteSelfReferencingIds(cls); convertToAuditBase(cls); addCustomInputs(cls); if (groupName) { if (!cls.annotations) cls.annotations = []; if (!cls.annotations.some(x => x.name === 'tag')) { cls.annotations.push({ name: 'tag', constructorArgs: [groupName] }); } } }); changeUserRefsToAuditBase(tsAst); return tsAst; } // Replace User Tables and FKs with AuditBase tables and export function transformUserRefs(tsAst, info) { const addClasses = []; for (const cls of tsAst.classes) { const removeProps = []; for (const prop of cls.properties) { const propLower = prop.name.toLowerCase(); if (propLower === 'userid') { removeProps.push(prop.name); } if (prop.type === 'User') { cls.extends = 'AuditBase'; // Replace with local User Type /* [Reference(SelfId = nameof(CreatedBy), RefId = nameof(User.UserName), RefLabel = nameof(User.DisplayName))] public User UserRef { get; set; } */ if (info.userType) prop.type = info.userType; if (!prop.annotations) prop.annotations = []; if (!prop.annotations.find(x => x.name === 'reference')) { const attr = { name: 'reference', args: { selfId: 'createdBy', refId: 'userName', } }; if (info.userLabel) attr.args.refLabel = info.userLabel; prop.annotations.push(attr); } } if (prop.type === 'User[]') { if (info.userType) { prop.type = info.userType + '[]'; if (!prop.annotations) prop.annotations = []; if (!prop.annotations.find(x => x.name === 'reference')) { prop.annotations.push({ name: 'reference' }); } const idPropType = cls.properties.find(x => x.name.toLowerCase() === 'id')?.type ?? 'number'; // Add Many to Many User Table addClasses.push({ name: cls.name + 'User', properties: [ { name: 'id', type: 'number' }, { name: toCamelCase(cls.name) + 'Id', type: idPropType }, { name: toCamelCase(info.userType) + 'Id', type: info.userIdType ?? 'string' }, ] }); } } } if (removeProps.length) { cls.extends = 'AuditBase'; cls.properties = cls.properties.filter(x => !removeProps.includes(x.name)); } } if (addClasses.length) { tsAst.classes.push(...addClasses); } // Remove User Table if local User Table exists if (info.userType) { tsAst.classes = tsAst.classes.filter(x => x.name !== 'User'); } } function replaceUserBaseClass(type) { if (type.extends === 'User') { type.extends = 'AuditBase'; let idProp = type.properties?.find(x => x.name.toLowerCase() === 'id'); if (!idProp) { idProp = { name: 'id', type: 'number' }; type.properties?.unshift(idProp); } } } function replaceConflictTypes(gen) { const conflictTypeNames = [`Service`, `Task`]; const conflictTypes = gen.classes.filter(x => conflictTypeNames.includes(x.name)); for (const conflictType of conflictTypes) { const firstFkProp = conflictType.properties?.find(x => x.name.endsWith('Id')); if (firstFkProp) { const splitWords = splitCase(firstFkProp.name).split(' '); const prefix = splitWords.length >= 2 ? splitWords[splitWords.length - 2] : null; if (prefix) { const newType = `${toPascalCase(prefix)}${conflictType.name}`; const conflictTypeName = conflictType.name; conflictType.name = newType; for (const type of gen.classes) { if (type.properties) { for (const prop of type.properties) { if (prop.type === conflictTypeName) { prop.type = newType; } if (prop.type === `${conflictTypeName}[]`) { prop.type = `${newType}[]`; } } } } } } } } function rewriteDuplicateTypePropNames(type) { const duplicateTypePropMap = { note: 'content', }; const duplicateProp = type.properties?.find(x => toPascalCase(x.name) === type.name); if (duplicateProp) { const newName = duplicateTypePropMap[duplicateProp.name] ?? 'value'; duplicateProp.name = newName; } } function removeDuplicateProps(type) { const propNames = type.properties?.map(x => x.name) ?? []; const duplicateProps = type.properties?.filter(x => propNames.filter(y => y === x.name).length > 1); if (duplicateProps) { const props = []; for (const prop of type.properties) { if (!props.find(x => x.name === prop.name)) { props.push(prop); } } type.properties = props; } } function rewriteSelfReferencingIds(type) { const selfRefId = type.properties?.find(x => x.name.toLowerCase() === `${type.name}id`.toLowerCase()); if (selfRefId) { selfRefId.name = `parentId`; } } export function addCustomInputs(cls) { const currencyTypeProps = [ "price", "cost", "total", "salary", "balance", "tax", "fee" ]; for (const prop of cls.properties ?? []) { if (currencyTypeProps.some(x => prop.name.toLowerCase().includes(x)) && (prop.type === 'number' || prop.type === 'decimal' || prop.type === 'double')) { if (!prop.annotations) prop.annotations = []; prop.annotations.push({ name: "intlNumber", args: { currency: "USD" } }); } } } function rewriteToPascalCase(ast) { ast?.classes.forEach(t => { t.name = toPascalCase(t.name); t.properties?.forEach(p => p.name = toCamelCase(p.name)); }); ast.enums?.forEach(e => { e.name = toPascalCase(e.name); if (e.members?.length) { e.members?.forEach((m, i) => { // always reset the value of AI enums m.value = i; m.name = toPascalCase(m.name); }); } }); } function rewriteInterfaceNames(ast) { const renameTypes = {}; ast.interfaces?.forEach(t => { const hasIPrefix = t.name.startsWith('I') && t.name[1].toUpperCase() === t.name[1] && t.name[2].toLowerCase() === t.name[2]; if (hasIPrefix) { renameTypes[t.name] = t.name.substring(1); renameTypes[t.name + '[]'] = t.name.substring(1) + '[]'; t.name = t.name.substring(1); } }); ast.classes?.concat(ast.interfaces || []).forEach(t => { if (renameTypes[t.extends]) { t.extends = renameTypes[t.extends]; } t.properties?.forEach(p => { if (renameTypes[p.type]) { p.type = renameTypes[p.type]; } }); }); } function convertToAuditBase(cls) { const props = cls.properties ?? []; const auditFields = ['createdBy', 'updatedBy', 'createdAt', 'updatedAt', 'userId']; if (props.find(x => auditFields.includes(x.name))) { cls.extends = 'AuditBase'; cls.properties = props.filter(x => !auditFields.includes(x.name)); } } function changeUserRefsToAuditBase(ast) { const user = ast.classes?.find(x => x.name === 'User') ?? ast.interfaces?.find(x => x.name === 'User'); if (user) { user.properties?.forEach(prop => { // If User Model references any other model, chnage it to extend AuditBase const ref = ast.classes?.find(x => x.name === prop.type) ?? ast.interfaces?.find(x => x.name === prop.type); if (ref) { ref.extends = 'AuditBase'; } // If User Model references any other model[], chnage it to extend AuditBase const refs = ast.classes?.find(x => prop.type.endsWith('[]') && x.name === prop.type.substring(0, prop.type.length - 2)) ?? ast.interfaces?.find(x => prop.type.endsWith('[]') && x.name === prop.type.substring(0, prop.type.length - 2)); if (refs) { refs.extends = 'AuditBase'; } }); } } function mergeInterfacesAndClasses(ast) { const classes = []; for (const iface of ast.interfaces) { const cls = interfaceToClass(iface); classes.push(cls); } for (const cls of ast.classes) { classes.push(cls); } ast.classes = classes; ast.interfaces = []; } function interfaceToClass(ast) { return pick(ast, ['name', 'extends', 'comment', 'properties', 'annotations']); } export function replaceReferences(tsAst) { const references = [ 'Service', 'Task', ]; // The most important types are the ones with the most references const refCount = (t) => t.properties?.filter(p => tsAst.classes.find(x => x.name === p.type)).length || 0; const importantTypes = tsAst.classes.sort((x, y) => refCount(y) - refCount(x)); for (const cls of tsAst.classes) { if (references.includes(cls.name)) { const importantType = importantTypes.find(x => x.properties?.some(p => p.type === cls.name)); if (importantType) { const newName = `${importantType.name}${cls.name}`; replaceReference(tsAst, cls.name, newName); } } } } export function replaceReference(gen, fromType, toType) { for (const cls of gen.classes) { if (cls.name === fromType) { cls.name = toType; } if (cls.properties) { for (const prop of cls.properties) { if (prop.type === fromType) { prop.type = toType; } if (prop.type === `${fromType}[]`) { prop.type = `${toType}[]`; } 2; if (prop.name === fromType) { prop.name = toType; } if (prop.name === `${fromType}Id`) { prop.name = `${toType}Id`; } } } } } export function replaceIds(gen) { for (const type of gen.classes) { const explicitIdProp = type.properties?.find(x => x.name.toLowerCase() === `${type.name}Id`.toLowerCase()); if (explicitIdProp) { const hasId = type.properties.find(x => x.name === 'id'); if (hasId) { explicitIdProp.name = 'parentId'; explicitIdProp.optional = true; } else { explicitIdProp.name = 'id'; } } else { // If using a shortened id for the type e.g. (PerformanceReview, ReviewId) const firstProp = type.properties?.[0]; if (firstProp?.name.endsWith('Id') && type.name.toLowerCase().includes(firstProp.name.substring(0, firstProp.name.length - 2).toLowerCase())) { firstProp.name = 'id'; } } } // Replace all string Ids with int Ids const anyIntPks = gen.classes.some(x => x.properties?.some(p => p.name === 'id' && p.type === 'number')); if (!anyIntPks) { for (const type of gen.classes) { const idProp = type.properties?.find(x => x.name === 'id'); if (idProp) { idProp.type = 'number'; } } } } //# sourceMappingURL=ts-once.js.map