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.
1,146 lines • 46.3 kB
JavaScript
import { getGroupName, lastLeftPart, leftPart, plural, rightPart, splitCase, toPascalCase } from "./utils.js";
import { getIcon } from "./icons.js";
const sys = (name, genericArgs) => ({ name, namespace: "System", genericArgs });
const sysObj = sys("object");
const sysDictObj = { name: "Dictionary", genericArgs: ["string", "object"], namespace: "System.Collections.Generic" };
export class CSharpAst {
typeMap = {
"number": sys("int"),
"string": sys("string"),
"boolean": sys("bool"),
"Date": sys("DateTime"),
"object": sysDictObj,
"symbol": sys("string"),
"null": sys("string"),
"undefined": sys("string"),
"bigint": sys("long"),
"any": sysObj,
"unknown": sysObj,
"Record": sys("Dictionary"),
"Buffer": sys("byte[]"),
"ArrayBuffer": sys("byte[]"),
"SharedArrayBuffer": sys("byte[]"),
"Int8Array": sys("sbyte[]"),
"UInt8Array": sys("byte[]"),
"Uint8ClampedArray": sys("byte[]"),
"Int16Array": sys("short[]"),
"UInt16Array": sys("ushort[]"),
"Int32Array": sys("int[]"),
"UInt32Array": sys("uint[]"),
"Float16Array": sys("float[]"),
"Float32Array": sys("float[]"),
"Float64Array": sys("double[]"),
"BigInt64Array": sys("long[]"),
"BigUInt64Array": sys("long[]"),
"Array": { name: "List", genericArgs: [], namespace: "System.Collections.Generic" },
"Map": { name: "Dictionary", genericArgs: [], namespace: "System.Collections.Generic" },
"WeakMap": { name: "Dictionary", genericArgs: [], namespace: "System.Collections.Generic" },
"Set": { name: "HashSet", genericArgs: [], namespace: "System.Collections.Generic" },
"WeakSet": { name: "HashSet", genericArgs: [], namespace: "System.Collections.Generic" },
};
decimalTypeProps = [
"price", "cost", "amount", "total", "salary", "balance", "rate", "discount", "tax", "fee"
];
valueTypes = [
"int", "long", "short", "ulong", "ushort", "sbyte", "uint", "char", "byte", "float", "double", "decimal", "bool",
"Int16", "Int32", "Int64", "UInt16", "UInt32", "UInt64", "SByte", "Byte", "Single", "Double", "Decimal", "Boolean",
"DateTime", "DateTimeOffset", "TimeSpan", "DateOnly", "TimeOnly", "Guid",
];
integerTypes = [
"int", "long", "short", "ulong", "ushort", "sbyte", "uint",
"Int16", "Int32", "Int64", "UInt16", "UInt32", "UInt64"
];
commonValueTypes = [
"int", "Int32", "long", "Int64", "string",
];
requestAttrs = [
'api',
'apiResponse',
'validateRequest',
'validateIsAuthenticated',
'validateIsAdmin',
'validateAuthSecret',
'validateHasRole',
'validateHasRoles',
'validateHasPermission',
'validateHasPermissions',
'validateHasClaim',
'validateHasScope',
'validateApiKey',
'queryDb',
'queryData',
'autoFilter',
'autoPopulate',
'route',
'field',
'tag',
'notes',
'description',
'meta',
'dataContract',
].map(x => x.toLowerCase());
modelAttrs = [
'schema',
'compositeKey',
'preCreateTable',
'preDropTable',
'postCreateTable',
'postDropTable',
'namedConnection',
'alias',
'icon',
'meta',
'dataContract',
'notes',
'description',
].map(x => x.toLowerCase());
requestPropAttrs = [
'validate',
'validateNull',
'validateEmpty',
'validateEmail',
'validateNotNull',
'validateNotEmpty',
'validateCreditCard',
'validateLength',
'validateExactLength',
'validateMinimumLength',
'validateMaximumLength',
'validateLessThan',
'validateLessThanOrEqual',
'validateGreaterThan',
'validateGreaterThanOrEqual',
'validateScalePrecision',
'validateRegularExpression',
'validateEqualExpression',
'validateNotEqualExpression',
'validateInclusiveBetween',
'validateExclusiveBetween',
'allowReset',
'denyReset',
'queryDbField',
'queryDataField',
'autoUpdate',
'autoDefault',
'autoMap',
'autoIgnore',
'autoApply',
'apiMember',
'apiAllowableValues',
'dataMember',
'input',
'fieldCss',
'uploadTo',
].map(x => x.toLowerCase());
get requestPropAttrsWithoutValidators() {
return this.requestPropAttrs.filter(x => !x.startsWith('validate'));
}
modelPropAttrs = [
'alias',
'meta',
'priority',
'primaryKey',
'autoId',
'autoIncrement',
'index',
'compute',
'computed',
'persisted',
'uniqueConstraint',
'addColumn',
'removeColumn',
'belongTo',
'checkConstraint',
'customField',
'customSelect',
'customInsert',
'customUpdate',
'decimalLength',
'Default',
'description',
'enumAsInt',
'excludeMetadata',
'excludeFromDescription',
'explicitAutoQuery',
'foreignKey',
'ignore',
'ignoreOnUpdate',
'ignoreOnInsert',
'ignoreDataMember',
'reference',
'referenceField',
'references',
'required',
'returnOnInsert',
'rowVersion',
'unique',
'dataMember',
'ref',
'format',
'intl',
'intlNumber',
'intlDateTime',
'intlRelativeTime',
].map(x => x.toLowerCase());
// Only generate these attributes when it's API specific, e.g. Read.notes("...")
targetOnlyAttrs = [
'notes',
'description',
].map(x => x.toLowerCase());
// Ignore properties with these attributes on APIs
ignoreCreateProps = [
'autoIncrement',
'reference',
'compute',
'computed',
].map(x => x.toLowerCase());
ignoreUpdateProps = [
'reference',
'compute',
'computed',
].map(x => x.toLowerCase());
// Validators that should be on Create but not optional Update APIs
ignoreUpdateValidators = [
'validateNull',
'validateNotNull',
'validateEmpty',
'validateNotEmpty',
].map(x => x.toLowerCase());
ignoreReadValidators = this.requestPropAttrs.filter(x => x.startsWith('validate')).map(x => x.toLowerCase());
ignoreDeleteValidators = this.requestPropAttrs.filter(x => x.startsWith('validate')).map(x => x.toLowerCase());
ignoreApisFor = [
'User',
];
isEnum(type) {
type = unwrap(type);
return this.ast.enums.some(x => x.name === type) || this.result.types.find(x => x.name === type && x.isEnum);
}
isValueType(type) {
type = unwrap(type);
return this.valueTypes.includes(type) || this.isEnum(type);
}
ast = { references: [], classes: [], interfaces: [], enums: [] };
result = {
config: {},
namespaces: [],
operations: [],
types: []
};
toCsName(tsName) {
return toPascalCase(tsName);
}
csharpType(type, propName) {
if (type.endsWith('[]')) {
const elType = this.csharpType(type.substring(0, type.length - 2), propName);
return Object.assign({}, elType, { name: elType.name + '[]' });
}
if (type.endsWith('>')) {
const start = type.indexOf('<');
const genericArgs = type.substring(start + 1, type.length - 1).split(',').map(x => this.csharpType(x.trim()).name);
const baseType = this.csharpType(type.substring(0, start), propName);
const ret = Object.assign({}, baseType, { genericArgs });
return ret;
}
if (propName) {
if (type === "number") {
if (this.decimalTypeProps.some(x => propName.toLowerCase().includes(x))) {
return sys("decimal");
}
}
}
return this.typeMap[type] ?? { name: type, namespace: "MyApp" };
}
csharpAttribute(attr) {
const attrName = attr.name.includes('.')
? rightPart(attr.name, '.')
: attr.name;
const to = { name: toPascalCase(attrName) };
const attrType = (value) => typeof value == 'string'
? ((value.startsWith('typeof(') || value.startsWith('nameof(')) && value.endsWith(')')
? "constant" :
value.match(/^[A-Z][A-Za-z0-9_]+\.[A-Z][A-Za-z0-9_]+$/) //Axx.Bxx
? "constant"
: "string")
: typeof value == "object"
? (value instanceof Date ? "string" : Array.isArray(value) ? "array" : "object")
: typeof value;
if (attr.constructorArgs?.length) {
to.constructorArgs = attr.constructorArgs.map(x => {
const type = attrType(x);
return {
name: 'String',
type,
value: `${x}`
};
});
}
if (attr.args && Object.keys(attr.args).length) {
to.args = Object.entries(attr.args).map(([name, value]) => {
const type = attrType(value);
return {
name: toPascalCase(name),
type,
value: `${value}`
};
});
}
if (attr.name.includes('.')) {
to.namespace = leftPart(attr.name, '.');
}
return to;
}
addMetadataType(cls) {
const type = {
name: this.toCsName(cls.name),
namespace: "MyApp",
properties: cls.properties.map(p => {
const type = this.csharpType(p.type, p.name);
const prop = {
name: this.toCsName(p.name),
type: p.optional ? nullable(type.name) : type.name,
};
if (type.namespace) {
prop.namespace = type.namespace;
}
if (type.genericArgs) {
prop.genericArgs = type.genericArgs;
}
if (p.comment) {
prop.description = p.comment;
}
if (prop.name === 'Id') {
prop.isPrimaryKey = true;
prop.isRequired = true;
}
const valueType = this.isValueType(prop.type);
if (valueType) {
prop.isValueType = true;
}
const isEnum = this.isEnum(prop.type);
if (isEnum) {
prop.isEnum = true;
}
if (!prop.isValueType && !prop.type.endsWith('?')) {
prop.isRequired = true;
}
if (p.annotations?.length) {
prop.attributes = p.annotations.map(x => this.csharpAttribute(x));
}
return prop;
}),
};
if (cls.comment) {
type.description = cls.comment;
}
if (cls.extends) {
type.inherits = { name: this.toCsName(cls.extends) };
}
if (cls.implements?.length) {
type.implements = cls.implements.filter(x => !x.startsWith("IReturn")).map(x => this.csharpType(x));
}
if (cls.annotations?.length) {
type.attributes = cls.annotations.map(x => this.csharpAttribute(x));
}
// Add dependent types first
type.properties.filter(x => x.namespace === 'MyApp'
&& x.name !== cls.name
&& !this.result.types.some(t => t.name === x.name && t.namespace === 'MyApp')).forEach(x => {
const refEnum = this.ast.enums.find(e => e.name === x.type);
if (refEnum) {
this.addMetadataEnum(refEnum);
}
const refType = this.ast.classes.find(c => c.name === x.type);
if (refType) {
this.addMetadataType(refType);
}
});
if (isDataModel(type) || type.isEnum) {
if (!this.result.types.find(x => x.name === cls.name && x.namespace === 'MyApp')) {
this.result.types.push(type);
}
}
else {
if (!this.result.operations.find(x => x.request.name === cls.name)) {
let method = "POST";
if (cls.extends?.startsWith('Query')) {
method = "GET";
}
let impls = cls.implements;
if (impls?.length) {
if (impls.includes('ICreate')) {
method = "POST";
}
else if (impls.includes('IUpdate')) {
method = "PUT";
}
else if (impls.includes('IPatch')) {
method = "PATCH";
}
else if (impls.includes('IDelete')) {
method = "DELETE";
}
}
const op = {
request: type,
actions: ["ANY"],
method,
};
const iReturn = impls?.find(x => x.startsWith('IReturn<'));
if (iReturn) {
op.returnType = this.csharpType(lastLeftPart(rightPart(iReturn, '<'), '>'));
}
if (impls?.includes('IReturnVoid')) {
op.returnsVoid = true;
}
this.result.operations.push(op);
}
}
return type;
}
addMetadataEnum(e) {
if (this.result.types.find(x => x.name === e.name && x.isEnum))
return;
const type = {
name: this.toCsName(e.name),
namespace: "MyApp",
description: e.comment,
isEnum: true,
isEnumInt: typeof e.members[0].value == 'number',
enumNames: e.members.map(x => this.toCsName(x.name)),
};
if (type.isEnumInt) {
type.enumValues = e.members.map(x => `${x.value}`);
}
else {
type.enumMemberValues = e.members.map(x => `${x.value}`);
}
if (e.members.some(x => x.comment)) {
type.enumDescriptions = e.members.map(x => x.comment);
}
this.result.types.push(type);
return type;
}
get classes() {
return this.result.types.filter(x => !x.isEnum && x.properties);
}
// e.g. Table DataModels have Primary Keys
get typesWithPrimaryKeys() {
return this.result.types.filter(x => !x.isEnum && !x.isInterface && x.properties?.some(x => x.isPrimaryKey));
}
get typesWithReferences() {
return this.result.types.filter(x => !x.isEnum && !x.isInterface && x.properties?.some(x => x.attributes?.some(x => x.name === 'Reference')));
}
onlyAttrs(attrs, only) {
if (!attrs)
return;
return attrs.filter(x => only.includes(x.name.toLowerCase()));
}
attrsFor(dtoType, attrType, attrs) {
const requestAttrs = this.requestAttrs;
const requestPropAttrs = this.requestPropAttrs;
const modelAttrs = this.modelAttrs;
const modelPropAttrs = this.modelPropAttrs;
const isRequest = ["Read", "Create", "Update", "Delete"].includes(dtoType);
const validAttrs = attrType === "Type"
? isRequest
? requestAttrs
: modelAttrs
: isRequest
? requestPropAttrs
: modelPropAttrs;
const ignoreValidators = attrType === "Prop"
? dtoType === "Update"
? this.ignoreUpdateValidators
: dtoType === "Read"
? this.ignoreReadValidators
: dtoType === "Delete"
? this.ignoreDeleteValidators
: []
: [];
const targetOnlyAttrs = this.targetOnlyAttrs;
function shouldInclude(attr, dtoType) {
if (!validAttrs.includes(attr.name.toLowerCase()))
return false;
const ns = attr.namespace;
if (ns) {
if (ns == "All")
return true;
if (ns == "Read")
return dtoType == "Read";
if (ns == "Create")
return dtoType == "Create";
if (ns == "Update")
return dtoType == "Update";
if (ns == "Delete")
return dtoType == "Delete";
if (ns == "Write")
return ["Create", "Update", "Delete"].includes(dtoType);
return false;
}
else {
const nameLower = attr.name.toLowerCase();
if (isRequest) {
if (attrType === "Type") {
if (targetOnlyAttrs.includes(nameLower)) {
return false;
}
return requestAttrs.includes(nameLower);
}
else if (attrType === "Prop") {
if (ignoreValidators.length && ignoreValidators.includes(nameLower))
return false;
return requestPropAttrs.includes(nameLower);
}
}
else {
if (attrType === "Type") {
return modelAttrs.includes(nameLower);
}
else if (attrType === "Prop") {
return modelPropAttrs.includes(nameLower);
}
}
}
return true;
}
const to = [];
for (const attr of attrs || []) {
if (shouldInclude(attr, dtoType)) {
to.push(attr);
}
}
return to;
}
createAutoCrudApis() {
const groupName = getGroupName(this.result);
const friendlyGroupName = splitCase(groupName);
for (const type of this.classes) {
if (!isDataModel(type))
continue;
if (this.ignoreApisFor.includes(type.name))
continue;
const hasPk = type.properties?.some(x => x.isPrimaryKey);
if (!hasPk)
continue;
const pluralizedType = plural(type.name);
const queryName = `Query${pluralizedType}`;
let queryApi = this.result.operations.find(x => x.request.name === queryName
|| (x.request.inherits?.name.startsWith('Query') && x.request.inherits?.genericArgs?.some(a => a === type.name)));
const pk = type.properties?.find(x => x.isPrimaryKey);
const dataModel = { name: type.name, namespace: type.name };
const isAuditBase = type.inherits?.name === 'AuditBase';
const existingTag = type.attributes?.find(x => x.name.toLowerCase() === 'tag');
const tags = !existingTag ? [friendlyGroupName] : undefined;
const emptyTag = existingTag?.constructorArgs?.[0]?.value === '';
if (emptyTag) {
type.attributes = type.attributes.filter(x => x !== existingTag);
}
const inputTagAttrs = [{
name: "Input",
args: [{ name: "Type", type: "string", value: "tag" }]
},
{
name: "FieldCss",
args: [{ name: "Field", type: "string", value: "col-span-12" }]
}];
const idsProp = pk
? {
name: `${pk.name}s`,
type: "List`1?",
namespace: "System.Collections.Generic",
genericArgs: [pk.type]
}
: undefined;
if (!queryApi) {
queryApi = {
method: "GET",
actions: ["ANY"],
routes: [],
request: {
name: queryName,
namespace: "MyApp",
inherits: {
name: "QueryDb`1",
namespace: "ServiceStack",
genericArgs: [type.name]
},
properties: pk
? [
Object.assign({}, pk, {
type: nullable(pk.type),
attributes: this.attrsFor("Read", "Prop", pk.attributes),
}),
idsProp
]
: [],
attributes: this.attrsFor("Read", "Type", type.attributes),
},
returnType: {
name: "QueryResponse`1",
namespace: "ServiceStack",
genericArgs: [type.name]
},
dataModel,
};
if (isAuditBase) {
if (!queryApi.request.attributes?.find(x => x.name === 'AutoApply')) {
// [AutoApply(Behavior.AuditQuery)]
queryApi.request.attributes.push({
name: "AutoApply",
constructorArgs: [{
name: "name",
type: "constant",
value: "Behavior.AuditQuery"
}]
});
}
}
this.result.operations.push(queryApi);
}
let createName = `Create${type.name}`;
let createApi = this.result.operations.find(x => x.request.name === createName
|| (x.request.implements?.some(i => i.name.startsWith("ICreate") && x.dataModel == dataModel)));
if (!createApi) {
createApi = {
method: "POST",
actions: ["ANY"],
request: {
name: createName,
namespace: "MyApp",
implements: [{
name: "ICreateDb`1",
namespace: "ServiceStack",
genericArgs: [type.name]
}],
properties: type.properties
.filter(x => !x.attributes?.some(a => this.ignoreCreateProps.includes(a.name.toLowerCase()))).map(x => Object.assign({}, x, {
type: x.isPrimaryKey
? x.type
: `${x.type}`,
attributes: this.attrsFor("Create", "Prop", x.attributes),
})),
attributes: this.attrsFor("Create", "Type", type.attributes),
},
returnType: {
name: "IdResponse",
namespace: "ServiceStack",
},
dataModel,
};
for (const prop of createApi.request.properties) {
if (prop.isRequired) {
if (!prop.attributes)
prop.attributes = [];
var hasAnyValidateAttrs = prop.attributes.some(x => x.name.toLowerCase().startsWith('validate'));
if (prop.type === 'string') {
if (!hasAnyValidateAttrs) {
prop.attributes.push({
name: "ValidateNotEmpty",
});
}
}
else if (this.integerTypes.includes(prop.type)) {
if (!hasAnyValidateAttrs) {
prop.attributes.push({
name: "ValidateGreaterThan",
constructorArgs: [{ name: "value", type: "int", value: "0" }]
});
}
}
else if (prop.type === 'List`1' && this.commonValueTypes.includes(prop.genericArgs[0])) {
prop.attributes.push(...inputTagAttrs);
}
const emptyValidateAttr = prop.attributes
.find(x => x.name.toLowerCase() === 'validate' && !x.constructorArgs?.length && !x.args?.length);
if (emptyValidateAttr) {
prop.attributes = prop.attributes.filter(x => x !== emptyValidateAttr);
}
}
}
if (isAuditBase) {
createApi.requiresAuth = true;
if (!createApi.request.attributes?.find(x => x.name === 'AutoApply')) {
// [AutoApply(Behavior.AuditCreate)]
createApi.request.attributes.push({
name: "AutoApply",
constructorArgs: [{
name: "name",
type: "constant",
value: "Behavior.AuditCreate"
}]
});
}
}
this.result.operations.push(createApi);
}
let updateName = `Update${type.name}`;
let updateApi = this.result.operations.find(x => x.request.name === updateName
|| x.dataModel == dataModel && (x.request.implements?.some(i => i.name.startsWith("IPatch")) ||
x.request.implements?.some(i => i.name.startsWith("IUpdate"))));
if (!updateApi) {
updateApi = {
method: "PATCH",
actions: ["ANY"],
request: {
name: updateName,
namespace: "MyApp",
implements: [{
name: "IPatchDb`1",
namespace: "ServiceStack",
genericArgs: [type.name]
}],
properties: type.properties?.filter(x => !x.attributes?.some(x => this.ignoreUpdateProps.includes(x.name.toLowerCase()))).map(x => Object.assign({}, x, {
type: x.isPrimaryKey
? x.type
: `${nullable(x.type)}`,
attributes: this.attrsFor("Update", "Prop", x.attributes),
})),
attributes: this.attrsFor("Update", "Type", type.attributes),
},
returnType: {
name: "IdResponse",
namespace: "ServiceStack",
},
dataModel,
};
for (const prop of updateApi.request.properties) {
if (prop.isRequired) {
if (!prop.attributes)
prop.attributes = [];
if (prop.type === 'List`1' && this.commonValueTypes.includes(prop.genericArgs[0])) {
prop.attributes.push(...inputTagAttrs);
}
}
const emptyValidateAttr = prop.attributes
.find(x => x.name.toLowerCase() === 'validate' && !x.constructorArgs?.length && !x.args?.length);
if (emptyValidateAttr) {
prop.attributes = prop.attributes.filter(x => x !== emptyValidateAttr);
}
}
if (isAuditBase) {
updateApi.requiresAuth = true;
if (!updateApi.request.attributes?.find(x => x.name === 'AutoApply')) {
// [AutoApply(Behavior.AuditModify)]
updateApi.request.attributes.push({
name: "AutoApply",
constructorArgs: [{
name: "name",
type: "constant",
value: "Behavior.AuditModify"
}]
});
}
}
this.result.operations.push(updateApi);
}
let deleteName = `Delete${type.name}`;
let deleteApi = this.result.operations.find(x => x.request.name === deleteName
|| (x.request.implements?.some(i => i.name.startsWith("IDelete") && x.dataModel == dataModel)));
if (!deleteApi) {
deleteApi = {
method: "DELETE",
actions: ["ANY"],
request: {
name: deleteName,
namespace: "MyApp",
implements: [{
name: "IDeleteDb`1",
namespace: "ServiceStack",
genericArgs: [type.name]
}],
properties: pk
? [
Object.assign({}, pk, {
type: nullable(pk.type),
attributes: this.attrsFor("Delete", "Prop", pk.attributes),
}),
idsProp
]
: [],
attributes: this.attrsFor("Delete", "Type", type.attributes),
},
returnsVoid: true,
dataModel,
};
if (isAuditBase) {
deleteApi.requiresAuth = true;
if (!deleteApi.request.attributes?.find(x => x.name === 'AutoApply')) {
// [AutoApply(Behavior.AuditSoftDelete)]
deleteApi.request.attributes.push({
name: "AutoApply",
constructorArgs: [{
name: "name",
type: "constant",
value: "Behavior.AuditSoftDelete"
}]
});
}
}
this.result.operations.push(deleteApi);
}
}
this.filterModelAttributes();
return this.result;
}
filterModelAttributes() {
for (const type of this.classes) {
if (!isDataModel(type))
continue;
if (type.attributes?.length) {
type.attributes = this.attrsFor("Model", "Type", type.attributes);
}
type.properties?.forEach(p => {
if (p.attributes?.length) {
p.attributes = this.attrsFor("Model", "Prop", p.attributes);
}
});
}
}
parseTypes() {
this.ast.classes.forEach(c => {
const name = toPascalCase(c.name);
if (this.result.types.find(x => x.name === name && x.namespace === 'MyApp'))
return;
this.addMetadataType(c);
});
this.ast.enums.forEach(e => {
const name = toPascalCase(e.name);
if (this.result.types.find(x => x.name === name && x.isEnum))
return;
this.addMetadataEnum(e);
});
}
parse(ast) {
const classes = ast.classes.concat(ast.interfaces);
const enums = ast.enums;
this.ast = {
references: [],
classes: classes,
interfaces: [],
enums: enums ?? [],
};
this.result = {
config: {},
namespaces: [
"System",
"System.IO",
"System.Collections",
"System.Collections.Generic",
"System.Runtime.Serialization",
"ServiceStack",
"ServiceStack.DataAnnotations",
],
operations: [],
types: [],
};
this.parseTypes();
return this.result;
}
}
export const Transforms = {
Default: [
convertReferenceTypes,
convertArrayReferenceTypes,
convertArraysToLists,
addMissingReferencesToForeignKeyProps,
addAutoIncrementAttrs,
addIconsToKnownTypes,
hideReferenceProperties,
removeEmptyAttributes,
],
};
export function toMetadataTypes(ast, transforms) {
const gen = new CSharpAst();
gen.parse(ast);
if (!transforms)
transforms = Transforms.Default;
for (const transform of transforms) {
transform(gen);
}
return gen.createAutoCrudApis();
}
export function unwrap(type) {
if (type.endsWith("?")) {
return type.substring(0, type.length - 1);
}
return type;
}
export function nullable(type) {
return type.endsWith('?') ? type : `${type}?`;
}
export function replaceReference(gen, fromType, toType) {
for (const type of gen.result.types) {
if (type.name === fromType) {
type.name = toType;
}
if (type.properties) {
for (const prop of type.properties) {
if (prop.type === fromType) {
prop.type = toType;
}
if (prop.name === fromType) {
prop.name = toType;
}
if (prop.name === `${fromType}Id`) {
prop.name = `${toType}Id`;
}
}
}
}
}
export function replaceReferences(gen, references) {
// The most important types are the ones with the most references
const refCount = (t) => t.properties?.filter(p => gen.result.types.find(x => x.name === p.type && p.namespace === 'MyApp')).length || 0;
const importantTypes = gen.result.types.sort((x, y) => refCount(y) - refCount(x));
for (const type of gen.result.types) {
if (references.includes(type.name)) {
const importantType = importantTypes.find(x => x.properties?.some(p => p.type === type.name));
if (importantType) {
const newName = `${importantType.name}${type.name}`;
replaceReference(gen, type.name, newName);
}
}
}
}
export function replaceIds(gen) {
for (const type of gen.classes) {
const idProp = type.properties?.find(x => x.name === `${type.name}Id`);
if (idProp) {
type.properties?.forEach(x => delete x.isPrimaryKey);
idProp.name = 'Id';
idProp.isPrimaryKey = true;
}
// If using a shortened id for the type e.g. (PerformanceReview, ReviewId)
const firstProp = type.properties?.[0];
if (firstProp?.name.endsWith('Id') && type.name.includes(firstProp.name.substring(0, firstProp.name.length - 2))) {
firstProp.name = 'Id';
firstProp.isPrimaryKey = true;
}
}
// Replace all string Ids with int Ids
const anyIntPks = gen.classes.some(x => x.properties?.some(p => p.isPrimaryKey && gen.integerTypes.includes(p.type)));
if (!anyIntPks) {
for (const type of gen.classes) {
const idProp = type.properties?.find(x => x.isPrimaryKey);
if (idProp) {
idProp.type = 'int';
}
}
}
}
export function removeEmptyAttributes(gen) {
const removeEmptyAttrsNamed = ['icon', 'description', 'notes', 'tag', 'icon', 'alias']; // 'validate' removed in gen APIs
const filterEmptyAttrs = (attrs) => attrs.filter(x => !(removeEmptyAttrsNamed.includes(x.name.toLowerCase()) && !x.constructorArgs?.length && !x.args?.length));
for (const type of gen.classes) {
if (type.attributes) {
type.attributes = filterEmptyAttrs(type.attributes);
}
if (type.properties) {
for (const prop of type.properties) {
if (prop.attributes) {
prop.attributes = filterEmptyAttrs(prop.attributes);
}
}
}
}
}
export function convertReferenceTypes(gen) {
for (const type of gen.classes) {
for (let i = 0; i < type.properties.length; i++) {
const p = type.properties[i];
const refType = gen.result.types.find(x => x.name === p.type && x.namespace === 'MyApp' && !x.isEnum);
if (refType) {
const fkId = `${p.name}Id`;
let idProp = refType.properties?.find(x => x.name === 'Id');
if (!idProp) {
idProp = { name: 'Id', type: 'int', isPrimaryKey: true, isRequired: true, isValueType: true, namespace: 'System' };
refType.properties?.unshift(idProp);
}
// Only add if FK Id prop does not already exist
if (!type.properties.find(x => x.name === fkId)) {
const fkProp = {
name: fkId,
type: idProp.type,
namespace: idProp.namespace,
attributes: [{
name: "References",
constructorArgs: [{
name: "type",
type: "Type",
value: `typeof(${p.type})`
}],
args: []
}]
};
type.properties.splice(i, 0, fkProp);
}
if (!p.attributes)
p.attributes = [];
p.attributes.push({ name: "Reference" });
i++; // Skip over added fk prop
}
}
}
}
export function convertArrayReferenceTypes(gen) {
for (const type of gen.classes) {
for (const prop of type.properties) {
if (prop.type.endsWith('[]')) {
const elType = prop.type.substring(0, prop.type.length - 2);
const refType = gen.result.types.find(x => x.name === elType && x.namespace === 'MyApp' && !x.isEnum);
if (refType && refType.properties?.find(x => x.name === 'Id' || x.isPrimaryKey)) {
prop.namespace = 'System.Collections.Generic';
prop.genericArgs = [elType];
prop.type = 'List`1';
if (!prop.attributes)
prop.attributes = [];
prop.attributes.push({ name: "Reference" });
let fkProp = refType.properties.find(x => x.name === `${type.name}Id`);
if (!fkProp) {
fkProp = {
name: `${type.name}Id`,
type: 'int',
isValueType: true,
namespace: 'System',
attributes: [{
name: "References",
constructorArgs: [{
name: "type",
type: "Type",
value: `typeof(${type.name})`
}]
}]
};
// Insert fk prop after last `*Id` prop
const lastIdPropIndex = refType.properties.findLastIndex(x => x.name.endsWith('Id'));
if (lastIdPropIndex >= 0) {
refType.properties.splice(lastIdPropIndex + 1, 0, fkProp);
}
else {
refType.properties.push(fkProp);
}
}
}
}
}
}
}
export function convertArraysToLists(gen) {
for (const type of gen.classes) {
for (const prop of type.properties) {
const optional = prop.type.endsWith('?');
let propType = unwrap(prop.type);
if (propType.endsWith('[]') && propType !== 'byte[]') {
const elType = propType.substring(0, propType.length - 2);
prop.namespace = 'System.Collections.Generic';
prop.genericArgs = [elType];
prop.type = 'List`1' + (optional ? '?' : '');
}
}
}
}
export function addMissingReferencesToForeignKeyProps(gen) {
for (const type of gen.typesWithPrimaryKeys) {
for (const prop of type.properties) {
if (prop.name.endsWith('Id') && !prop.isPrimaryKey && !prop.attributes?.some(x => x.name.startsWith('Reference'))) {
const refTypeName = prop.name.substring(0, prop.name.length - 2);
const refType = gen.result.types.find(x => x.name === refTypeName && x.namespace === 'MyApp' && !x.isEnum);
if (refType) {
if (!prop.attributes)
prop.attributes = [];
prop.attributes.push({
name: "References",
constructorArgs: [{
name: "type",
type: "Type",
value: `typeof(${refTypeName})`
}]
});
}
}
}
}
}
export function addAutoIncrementAttrs(gen) {
for (const type of gen.classes) {
for (const prop of type.properties) {
const hasPkAttr = prop.attributes?.some(x => ['primarykey', 'autoincrement', 'autoid'].includes(x.name.toLowerCase()));
if (prop.isPrimaryKey && !hasPkAttr) {
if (prop.type === 'int' || prop.type === 'long' || prop.type === 'Int32' || prop.type === 'Int64') {
if (!prop.attributes)
prop.attributes = [];
const attr = { name: "AutoIncrement" };
prop.attributes.push(attr);
}
}
}
}
}
// Add Icon for BuiltIn UIs and AutoQueryGrid to known type names
export function addIconsToKnownTypes(gen) {
for (const type of gen.typesWithPrimaryKeys) {
const icon = getIcon(type.name);
if (icon) {
if (!type.attributes)
type.attributes = [];
const existingIcon = type.attributes.find(x => x.name === 'Icon');
if (existingIcon) {
// remove empty icon
if (existingIcon.constructorArgs?.length === 0 && existingIcon.args?.length === 0) {
type.attributes = type.attributes.filter(x => x !== existingIcon);
}
return;
}
type.attributes.push({
name: "Icon",
args: [{ name: "Svg", type: "string", value: icon }]
});
}
}
}
// Hide Reference Properties from AutoQueryGrid Grid View
export function hideReferenceProperties(gen) {
for (const type of gen.typesWithReferences) {
for (const prop of type.properties.filter(x => x.attributes?.some(x => x.name === 'Reference'))) {
if (!prop.attributes)
prop.attributes = [];
//[Format(FormatMethods.Hidden)]
prop.attributes.push({
name: "Format",
constructorArgs: [{ name: "method", type: "constant", value: "FormatMethods.Hidden" }]
});
}
}
}
// Replace User Tables and FKs with AuditBase tables and
export function replaceUserReferencesWithAuditTables(gen) {
for (const type of gen.typesWithPrimaryKeys) {
const removeProps = [];
for (const prop of type.properties) {
if (prop.name === 'UserId') {
removeProps.push(prop.name);
}
if (prop.attributes?.some(a => a.name === 'Reference' && a.constructorArgs?.some(x => x.value === 'typeof(User)'))) {
removeProps.push(prop.name);
}
if (prop.type === 'User') {
removeProps.push(prop.name);
}
if (prop.genericArgs && prop.genericArgs.includes('User')) {
removeProps.push(prop.name);
}
}
if (removeProps.length) {
type.properties = type.properties.filter(x => !removeProps.includes(x.name));
type.inherits = { name: "AuditBase", namespace: "ServiceStack" };
}
}
// Remove User Table
gen.result.types = gen.result.types.filter(x => x.name !== 'User');
}
export function isDataModel(type) {
const apiInterfacePrefixes = ['IReturn', 'IQuery', 'ICreate', 'IUpdate', 'IPatch', 'IDelete', 'IGet', 'IPost', 'IPut'];
return (!type.inherits || !type.inherits.name.startsWith("Query"))
&& (!type.implements || !type.implements.some(x => apiInterfacePrefixes.some(prefix => x.name.startsWith(prefix))));
}
//# sourceMappingURL=cs-ast.js.map