UNPKG

@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
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