@seliseblocks/mcp-server
Version:
A Model Context Protocol (MCP) server for managing schemas in SELISE Blocks platform, built with TypeScript.
644 lines (604 loc) • 21.6 kB
JavaScript
export class TemplateGenerator {
constructor(request) {
this.request = request;
}
get pascalCaseName() {
return this.request.moduleName
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
}
get camelCaseName() {
return this.request.moduleName
.split('-')
.map((word, index) => index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1))
.join('');
}
get kebabCaseName() {
return this.request.moduleName;
}
get upperCaseName() {
return this.request.moduleName.toUpperCase().replace(/-/g, '_');
}
getFieldType(field) {
switch (field.type) {
case 'string':
case 'email':
case 'url':
return 'string';
case 'number':
return 'number';
case 'boolean':
return 'boolean';
case 'date':
return 'string';
default:
return 'string';
}
}
getZodType(field) {
switch (field.type) {
case 'string':
return 'z.string()';
case 'number':
return 'z.number()';
case 'boolean':
return 'z.boolean()';
case 'date':
return 'z.string()';
case 'email':
return 'z.string().email()';
case 'url':
return 'z.string().url()';
default:
return 'z.string()';
}
}
getFieldValidation(field) {
let validation = this.getZodType(field);
if (field.required) {
validation += '.min(1, \'This field is required\')';
}
return validation;
}
generateTypesFile() {
const fields = this.request.fields.map(field => ` ${field.name}: ${this.getFieldType(field)};`).join('\n');
const zodSchemaFields = this.request.fields.map(field => ` ${field.name}: ${this.getFieldValidation(field)},`).join('\n');
const content = `/**
* GraphQL Types for ${this.request.displayName} Management
*/
export interface ${this.pascalCaseName}Item {
ItemId: string;
${fields}
CreatedBy: string;
CreatedDate: string;
LastUpdatedBy: string;
LastUpdatedDate: string;
IsDeleted: boolean;
Language: string;
OrganizationIds: string[];
}
export interface Get${this.pascalCaseName}Response {
${this.pascalCaseName}s: {
items: ${this.pascalCaseName}Item[];
totalCount: number;
pageInfo: {
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string;
endCursor: string;
};
};
}
export interface Add${this.pascalCaseName}Input {
${this.request.fields.map(field => ` ${field.name}: ${this.getFieldType(field)};`).join('\n')}
}
export interface Add${this.pascalCaseName}Params {
input: Add${this.pascalCaseName}Input;
}
export interface Add${this.pascalCaseName}Response {
insert${this.pascalCaseName}: {
itemId: string;
totalImpactedData: number;
acknowledged: boolean;
};
}
// Zod schema for form validation
import * as z from 'zod';
export const ${this.camelCaseName}Schema = z.object({
${zodSchemaFields}
});
export type ${this.pascalCaseName}FormData = z.infer<typeof ${this.camelCaseName}Schema>;
`;
return {
path: `src/features/${this.kebabCaseName}/types/${this.kebabCaseName}.types.ts`,
content,
description: `TypeScript types and interfaces for ${this.request.displayName}`
};
}
generateQueriesFile() {
const fields = this.request.fields.map(field => ` ${field.name}`).join('\n');
const content = `/**
* GraphQL Queries for ${this.request.displayName} Management
*/
export const GET_${this.upperCaseName}_QUERY = \`
query ${this.pascalCaseName}s($input: DynamicQueryInput) {
${this.pascalCaseName}s(input: $input) {
hasNextPage
hasPreviousPage
totalCount
totalPages
pageSize
pageNo
items {
ItemId
${fields}
CreatedBy
CreatedDate
LastUpdatedBy
LastUpdatedDate
IsDeleted
Language
OrganizationIds
}
}
}
\`;
`;
return {
path: `src/features/${this.kebabCaseName}/graphql/queries.ts`,
content,
description: `GraphQL queries for ${this.request.displayName}`
};
}
generateMutationsFile() {
const content = `/**
* GraphQL Mutations for ${this.request.displayName} Management
*/
export const INSERT_${this.upperCaseName}_MUTATION = \`
mutation Insert${this.pascalCaseName}($input: ${this.pascalCaseName}InsertInput!) {
insert${this.pascalCaseName}(input: $input) {
itemId
totalImpactedData
acknowledged
}
}
\`;
export const UPDATE_${this.upperCaseName}_MUTATION = \`
mutation Update${this.pascalCaseName}($filter: String!, $input: ${this.pascalCaseName}UpdateInput!) {
update${this.pascalCaseName}(filter: $filter, input: $input) {
itemId
totalImpactedData
acknowledged
}
}
\`;
export const DELETE_${this.upperCaseName}_MUTATION = \`
mutation Delete${this.pascalCaseName}($filter: String!, $input: ${this.pascalCaseName}DeleteInput!) {
delete${this.pascalCaseName}(filter: $filter, input: $input) {
itemId
totalImpactedData
acknowledged
}
}
\`;
`;
return {
path: `src/features/${this.kebabCaseName}/graphql/mutations.ts`,
content,
description: `GraphQL mutations for ${this.request.displayName}`
};
}
generateServiceFile() {
const content = `import { graphqlClient } from 'lib/graphql-client';
import {
Add${this.pascalCaseName}Response,
Add${this.pascalCaseName}Params,
} from '../types/${this.kebabCaseName}.types';
import { GET_${this.upperCaseName}_QUERY } from '../graphql/queries';
import { INSERT_${this.upperCaseName}_MUTATION } from '../graphql/mutations';
export const get${this.pascalCaseName}s = async (context: {
queryKey: [string, { pageNo: number; pageSize: number }];
}) => {
const [, { pageNo, pageSize }] = context.queryKey;
return graphqlClient.query({
query: GET_${this.upperCaseName}_QUERY,
variables: {
input: {
filter: '{}',
sort: '{}',
pageNo,
pageSize,
},
},
});
};
export const add${this.pascalCaseName} = async (
params: Add${this.pascalCaseName}Params
): Promise<Add${this.pascalCaseName}Response> => {
const response = await graphqlClient.mutate<Add${this.pascalCaseName}Response>({
query: INSERT_${this.upperCaseName}_MUTATION,
variables: params,
});
return response;
};
`;
return {
path: `src/features/${this.kebabCaseName}/services/${this.kebabCaseName}.service.ts`,
content,
description: `Service layer for ${this.request.displayName}`
};
}
generateHooksFile() {
const content = `import { useGlobalQuery, useGlobalMutation } from 'state/query-client/hooks';
import { useQueryClient } from '@tanstack/react-query';
import { useToast } from 'hooks/use-toast';
import { useTranslation } from 'react-i18next';
import { Add${this.pascalCaseName}Params } from '../types/${this.kebabCaseName}.types';
import {
get${this.pascalCaseName}s,
add${this.pascalCaseName},
} from '../services/${this.kebabCaseName}.service';
interface ${this.pascalCaseName}QueryParams {
pageNo: number;
pageSize: number;
}
export const useGet${this.pascalCaseName}s = (params: ${this.pascalCaseName}QueryParams) => {
return useGlobalQuery({
queryKey: ['${this.kebabCaseName}', params],
queryFn: get${this.pascalCaseName}s,
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
refetchOnWindowFocus: false,
onError: (error) => {
throw error;
},
});
};
export const useAdd${this.pascalCaseName} = () => {
const queryClient = useQueryClient();
const { toast } = useToast();
const { t } = useTranslation();
return useGlobalMutation({
mutationFn: (params: Add${this.pascalCaseName}Params) => add${this.pascalCaseName}(params),
onSuccess: (data: any) => {
queryClient.invalidateQueries({
predicate: (query) => query.queryKey[0] === '${this.kebabCaseName}',
});
if (data.insert${this.pascalCaseName}?.acknowledged) {
toast({
variant: 'success',
title: t('${this.upperCaseName}_ITEM_ADDED'),
description: t('${this.upperCaseName}_ITEM_SUCCESSFULLY_CREATED'),
});
}
},
onError: (error) => {
throw error;
},
});
};
`;
return {
path: `src/features/${this.kebabCaseName}/hooks/use-${this.kebabCaseName}.ts`,
content,
description: `React Query hooks for ${this.request.displayName}`
};
}
generateFormComponent() {
const formFields = this.request.fields.map(field => {
const inputType = field.type === 'number' ? 'number' :
field.type === 'email' ? 'email' :
field.type === 'url' ? 'url' : 'text';
const stepAttr = field.type === 'number' ? ' step="0.01"' : '';
return ` <div className="space-y-2">
<Label htmlFor="${field.name}">${field.displayName}</Label>
<Input
id="${field.name}"
type="${inputType}"${stepAttr}
{...register('${field.name}'${field.type === 'number' ? ', { valueAsNumber: true }' : ''})}
placeholder="Enter ${field.displayName.toLowerCase()}"
/>
{errors.${field.name} && (
<p className="text-sm text-red-500">{errors.${field.name}.message}</p>
)}
</div>`;
}).join('\n\n');
const content = `import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Button } from 'components/ui/button';
import { Input } from 'components/ui/input';
import { Label } from 'components/ui/label';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from 'components/ui/card';
import { useAdd${this.pascalCaseName} } from '../../hooks/use-${this.kebabCaseName}';
import { Add${this.pascalCaseName}Input, ${this.camelCaseName}Schema, ${this.pascalCaseName}FormData } from '../../types/${this.kebabCaseName}.types';
interface ${this.pascalCaseName}FormProps {
onSuccess?: () => void;
onCancel?: () => void;
}
export function ${this.pascalCaseName}Form({ onSuccess, onCancel }: ${this.pascalCaseName}FormProps) {
const [isSubmitting, setIsSubmitting] = useState(false);
const { mutate: add${this.pascalCaseName}, isPending } = useAdd${this.pascalCaseName}();
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm<${this.pascalCaseName}FormData>({
resolver: zodResolver(${this.camelCaseName}Schema),
defaultValues: {
${this.request.fields.map(field => ` ${field.name}: ${field.type === 'number' ? '0' : field.type === 'boolean' ? 'false' : "''"}`).join(',\n')}
},
});
const onSubmit = (data: ${this.pascalCaseName}FormData) => {
setIsSubmitting(true);
const input: Add${this.pascalCaseName}Input = {
${this.request.fields.map(field => ` ${field.name}: data.${field.name}`).join(',\n')}
};
add${this.pascalCaseName}(
{ input },
{
onSuccess: () => {
reset();
setIsSubmitting(false);
onSuccess?.();
},
onError: () => {
setIsSubmitting(false);
},
}
);
};
return (
<Card className="w-full max-w-md mx-auto">
<CardHeader>
<CardTitle>Add ${this.request.displayName}</CardTitle>
<CardDescription>Enter the details for the new ${this.request.displayName.toLowerCase()} item</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
${formFields}
<div className="flex gap-2 pt-4">
<Button
type="submit"
disabled={isPending || isSubmitting}
className="flex-1"
>
{isPending || isSubmitting ? 'Adding...' : 'Add ${this.request.displayName}'}
</Button>
{onCancel && (
<Button
type="button"
variant="outline"
onClick={onCancel}
disabled={isPending || isSubmitting}
>
Cancel
</Button>
)}
</div>
</form>
</CardContent>
</Card>
);
}
`;
return {
path: `src/features/${this.kebabCaseName}/component/${this.kebabCaseName}-form/${this.kebabCaseName}-form.tsx`,
content,
description: `Form component for adding ${this.request.displayName} items`
};
}
generateIndexFile() {
const content = `export { ${this.pascalCaseName}Form } from './component/${this.kebabCaseName}-form/${this.kebabCaseName}-form';
`;
return {
path: `src/features/${this.kebabCaseName}/index.ts`,
content,
description: `Index file for ${this.request.displayName} feature exports`
};
}
generateColumnsFile() {
const columns = this.request.fields.map(field => {
let cellContent = `<div className="font-medium">{row.getValue('${field.name}')}</div>`;
if (field.type === 'number') {
cellContent = `{
const value = parseFloat(row.getValue('${field.name}'));
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(value);
return <div className="font-medium">{formatted}</div>;
}`;
}
else if (field.type === 'date') {
cellContent = `{
const date = new Date(row.getValue('${field.name}'));
return <div>{date.toLocaleDateString()}</div>;
}`;
}
else if (field.type === 'boolean') {
cellContent = `{
const value = row.getValue('${field.name}') as boolean;
return (
<div className="flex items-center">
<Badge variant={value ? 'default' : 'secondary'}>
{value ? 'Yes' : 'No'}
</Badge>
</div>
);
}`;
}
return ` {
accessorKey: '${field.name}',
header: ({ column }) => <DataTableColumnHeader column={column} title="${field.displayName}" />,
cell: ({ row }) => ${cellContent},
enableSorting: true,
},`;
}).join('\n');
const content = `import { ColumnDef } from '@tanstack/react-table';
import { ${this.pascalCaseName}Item } from 'features/${this.kebabCaseName}/types/${this.kebabCaseName}.types';
import { DataTableColumnHeader } from 'components/blocks/data-table/data-table-column-header';
export const create${this.pascalCaseName}Columns = (): ColumnDef<${this.pascalCaseName}Item>[] => [
${columns}
{
accessorKey: 'CreatedDate',
header: ({ column }) => <DataTableColumnHeader column={column} title="Created Date" />,
cell: ({ row }) => {
const date = new Date(row.getValue('CreatedDate'));
return <div>{date.toLocaleDateString()}</div>;
},
enableSorting: true,
},
];
`;
return {
path: `src/pages/${this.kebabCaseName}/${this.kebabCaseName}-columns.tsx`,
content,
description: `Data table columns configuration for ${this.request.displayName}`
};
}
generatePageFile() {
const content = `import { useCallback, useEffect, useState } from 'react';
import { Button } from 'components/ui/button';
import { Plus } from 'lucide-react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from 'components/ui/dialog';
import DataTable from 'components/blocks/data-table/data-table';
import { useGet${this.pascalCaseName}s } from 'features/${this.kebabCaseName}/hooks/use-${this.kebabCaseName}';
import { ${this.pascalCaseName}Item } from 'features/${this.kebabCaseName}/types/${this.kebabCaseName}.types';
import { ${this.pascalCaseName}Form } from 'features/${this.kebabCaseName}/component/${this.kebabCaseName}-form/${this.kebabCaseName}-form';
import { create${this.pascalCaseName}Columns } from './${this.kebabCaseName}-columns';
interface PaginationState {
pageIndex: number;
pageSize: number;
totalCount: number;
}
export function ${this.pascalCaseName}() {
const columns = create${this.pascalCaseName}Columns();
const [${this.camelCaseName}TableData, set${this.pascalCaseName}TableData] = useState<${this.pascalCaseName}Item[]>([]);
const [paginationState, setPaginationState] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
totalCount: 0,
});
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const {
data: ${this.camelCaseName}Data,
isLoading: is${this.pascalCaseName}Loading,
error: ${this.camelCaseName}Error,
} = useGet${this.pascalCaseName}Ites({
pageNo: paginationState.pageIndex + 1,
pageSize: paginationState.pageSize,
});
const data = ${this.camelCaseName}Data as { ${this.pascalCaseName}s: any };
useEffect(() => {
if (data?.${this.pascalCaseName}s?.items) {
const ${this.camelCaseName}DataMap = data.${this.pascalCaseName}s.items.map((item: ${this.pascalCaseName}Item) => ({
ItemId: item.ItemId,
${this.request.fields.map(field => ` ${field.name}: item.${field.name},`).join('\n')}
CreatedBy: item.CreatedBy,
CreatedDate: item.CreatedDate,
LastUpdatedBy: item.LastUpdatedBy,
LastUpdatedDate: item.LastUpdatedDate,
IsDeleted: item.IsDeleted,
Language: item.Language,
OrganizationIds: item.OrganizationIds,
}));
set${this.pascalCaseName}TableData(${this.camelCaseName}DataMap);
setPaginationState((prev) => ({
...prev,
totalCount: data.${this.pascalCaseName}s.totalCount ?? 0,
}));
}
}, [data]);
const handlePaginationChange = useCallback(
(newPagination: { pageIndex: number; pageSize: number }) => {
setPaginationState((prev) => ({
...prev,
pageIndex: newPagination.pageIndex,
pageSize: newPagination.pageSize,
}));
},
[]
);
const handleAddSuccess = () => {
setIsAddModalOpen(false);
};
return (
<div className="flex w-full flex-col space-y-4">
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold">${this.request.displayName}</h1>
<Dialog open={isAddModalOpen} onOpenChange={setIsAddModalOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" />
Add ${this.request.displayName}
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Add ${this.request.displayName}</DialogTitle>
</DialogHeader>
<${this.pascalCaseName}Form
onSuccess={handleAddSuccess}
onCancel={() => setIsAddModalOpen(false)}
/>
</DialogContent>
</Dialog>
</div>
<DataTable
data={${this.camelCaseName}TableData}
columns={columns}
isLoading={is${this.pascalCaseName}Loading}
error={${this.camelCaseName}Error instanceof Error ? ${this.camelCaseName}Error : null}
pagination={{
pageIndex: paginationState.pageIndex,
pageSize: paginationState.pageSize,
totalCount: paginationState.totalCount,
}}
manualPagination={true}
onPaginationChange={handlePaginationChange}
/>
</div>
);
}
`;
return {
path: `src/pages/${this.kebabCaseName}/${this.kebabCaseName}.tsx`,
content,
description: `Main page component for ${this.request.displayName}`
};
}
generateNavigationUpdate() {
const content = `// Add this line to src/constant/sidebar-menu.ts after the inventory menu item:
createMenuItem('${this.kebabCaseName}', '${this.upperCaseName}', '${this.request.route}', 'Store', { isIntegrated: true }),
// Add this line to src/App.tsx imports:
import { ${this.pascalCaseName} } from './pages/${this.kebabCaseName}/${this.kebabCaseName}';
// Add this line to src/App.tsx routes:
<Route path="${this.request.route}" element={<${this.pascalCaseName} />} />
// Add this line to src/i18n/route-module-map.ts:
'${this.request.route}': ['common', '${this.kebabCaseName}'],
`;
return {
path: `NAVIGATION_UPDATES.md`,
content,
description: `Manual navigation updates needed for ${this.request.displayName}`
};
}
generateAllFiles() {
const files = [];
files.push(this.generateTypesFile());
files.push(this.generateQueriesFile());
files.push(this.generateMutationsFile());
files.push(this.generateServiceFile());
files.push(this.generateHooksFile());
files.push(this.generateFormComponent());
files.push(this.generateIndexFile());
files.push(this.generateColumnsFile());
files.push(this.generatePageFile());
files.push(this.generateNavigationUpdate());
return files;
}
}
//# sourceMappingURL=templates.js.map