UNPKG

@wearesage/schema

Version:

A flexible schema definition and validation system for TypeScript with multi-database support

413 lines (330 loc) • 12.6 kB
import "reflect-metadata"; import { MetadataRegistry, SchemaBuilder, SchemaReflector } from "../../core"; import { MongoDBAdapter } from "../../adapters/mongodb"; import { Neo4jAdapter } from "../../adapters/neo4j"; import { PostgreSQLAdapter } from "../../adapters/postgresql"; import { Repository } from "../../adapters/repository"; // Import the blog entities import { User } from "../blog/User"; import { Post } from "../blog/Post"; import { Tag } from "../blog/Tag"; // GraphQL schema generation example async function main() { console.log("⚔ GraphQL Integration Example ⚔"); // Create a registry and builder const registry = new MetadataRegistry(); const builder = new SchemaBuilder(registry); const reflector = new SchemaReflector(registry); // Register all blog entities console.log("\nšŸ“ Registering blog entities..."); builder.registerEntities([User, Post, Tag]); // Create database adapters console.log("\nšŸ”Œ Creating database adapters..."); const mongoAdapter = new MongoDBAdapter(registry, "mongodb://localhost:27017/blog"); const neo4jAdapter = new Neo4jAdapter(registry, "bolt://localhost:7687"); const pgAdapter = new PostgreSQLAdapter(registry, "postgres://postgres:password@localhost:5432/blog"); // Create repositories console.log("\nšŸ“š Creating repositories..."); const userRepo = new Repository<User>(User, mongoAdapter, registry); const postRepo = new Repository<Post>(Post, neo4jAdapter, registry); const tagRepo = new Repository<Tag>(Tag, pgAdapter, registry); // Generate GraphQL type definitions console.log("\nšŸ“Š Generating GraphQL schema from entities..."); const graphqlTypes = generateGraphQLTypes(registry); console.log("\nGraphQL Type Definitions:"); console.log(graphqlTypes); // Generate GraphQL resolvers console.log("\nšŸ”§ Generating GraphQL resolvers..."); const resolvers = generateResolvers(registry, { User: userRepo, Post: postRepo, Tag: tagRepo }); console.log("\nGraphQL Resolver Structure:"); console.log(JSON.stringify(resolvers, null, 2)); console.log("\n✨ Example complete! ✨"); console.log("In a real application, you would use Apollo Server or another GraphQL server to expose this API."); } /** * Generate GraphQL type definitions from entity metadata */ function generateGraphQLTypes(registry: MetadataRegistry): string { let typeDefs = ""; // Add the Query type typeDefs += `type Query {\n`; // Get all registered entities const entities = registry.getAllEntities(); // Add query fields for each entity for (const [entityClass, entityOptions] of entities.entries()) { const entityName = entityClass.name; const pluralName = `${entityName.toLowerCase()}s`; typeDefs += ` ${entityName.toLowerCase()}(id: ID!): ${entityName}\n`; typeDefs += ` ${pluralName}: [${entityName}!]!\n`; } typeDefs += `}\n\n`; // Add the Mutation type typeDefs += `type Mutation {\n`; // Add mutation fields for each entity for (const [entityClass, entityOptions] of entities.entries()) { const entityName = entityClass.name; typeDefs += ` create${entityName}(input: Create${entityName}Input!): ${entityName}\n`; typeDefs += ` update${entityName}(id: ID!, input: Update${entityName}Input!): ${entityName}\n`; typeDefs += ` delete${entityName}(id: ID!): Boolean\n`; } typeDefs += `}\n\n`; // Generate types for each entity for (const [entityClass, entityOptions] of entities.entries()) { const entityName = entityClass.name; // Add the entity type typeDefs += `type ${entityName} {\n`; // Add ID field typeDefs += ` id: ID!\n`; // Add regular properties const properties = registry.getAllProperties(entityClass); if (properties) { for (const [propName, propOptions] of properties.entries()) { // Skip ID field as it's already added if (propName === 'id') continue; // Determine GraphQL type let gqlType = mapTypeToGraphQL(propOptions.type); // Add required flag if needed if (propOptions.required) { gqlType += '!'; } typeDefs += ` ${propName}: ${gqlType}\n`; } } // Add relationships const relationships = registry.getAllRelationships(entityClass); if (relationships) { for (const [relName, relOptions] of relationships.entries()) { const targetType = relOptions.target.name; // Determine if it's a to-many or to-one relationship const gqlType = relOptions.cardinality === 'many' ? `[${targetType}!]!` // to-many: returns an array : targetType; // to-one: returns a single entity typeDefs += ` ${relName}: ${gqlType}\n`; } } typeDefs += `}\n\n`; // Generate input types for mutations generateInputTypes(entityClass, registry, typeDefs); } return typeDefs; } /** * Generate GraphQL input types for an entity */ function generateInputTypes(entityClass: Function, registry: MetadataRegistry, typeDefs: string): string { const entityName = entityClass.name; // Create input type typeDefs += `input Create${entityName}Input {\n`; // Add regular properties const properties = registry.getAllProperties(entityClass); if (properties) { for (const [propName, propOptions] of properties.entries()) { // Skip ID field as it's typically auto-generated if (propName === 'id') continue; // Determine GraphQL type let gqlType = mapTypeToGraphQL(propOptions.type); // Add required flag if needed if (propOptions.required) { gqlType += '!'; } typeDefs += ` ${propName}: ${gqlType}\n`; } } // Add relationship IDs const relationships = registry.getAllRelationships(entityClass); if (relationships) { for (const [relName, relOptions] of relationships.entries()) { if (relOptions.cardinality === 'one') { // For to-one relationships, allow specifying the ID typeDefs += ` ${relName}Id: ID\n`; } else { // For to-many relationships, allow specifying an array of IDs typeDefs += ` ${relName}Ids: [ID!]\n`; } } } typeDefs += `}\n\n`; // Update input type (similar to create but all fields optional) typeDefs += `input Update${entityName}Input {\n`; // Add regular properties if (properties) { for (const [propName, propOptions] of properties.entries()) { // Skip ID field if (propName === 'id') continue; // Determine GraphQL type (all fields optional in update) const gqlType = mapTypeToGraphQL(propOptions.type); typeDefs += ` ${propName}: ${gqlType}\n`; } } // Add relationship IDs if (relationships) { for (const [relName, relOptions] of relationships.entries()) { if (relOptions.cardinality === 'one') { // For to-one relationships, allow specifying the ID typeDefs += ` ${relName}Id: ID\n`; } else { // For to-many relationships, allow specifying an array of IDs typeDefs += ` ${relName}Ids: [ID!]\n`; } } } typeDefs += `}\n\n`; return typeDefs; } /** * Map TypeScript/JavaScript types to GraphQL types */ function mapTypeToGraphQL(type: any): string { if (!type) return 'String'; // Default to String const typeName = type.name || String(type); switch (typeName) { case 'String': return 'String'; case 'Number': return 'Float'; case 'Boolean': return 'Boolean'; case 'Date': return 'DateTime'; default: return 'String'; } } /** * Generate GraphQL resolvers */ function generateResolvers(registry: MetadataRegistry, repositories: Record<string, Repository<any>>) { const entities = registry.getAllEntities(); const resolvers: any = { Query: {}, Mutation: {} }; // Add resolvers for each entity for (const [entityClass, entityOptions] of entities.entries()) { const entityName = entityClass.name; const pluralName = `${entityName.toLowerCase()}s`; const entityRepo = repositories[entityName]; if (!entityRepo) { console.warn(`No repository found for entity ${entityName}`); continue; } // Add Query resolvers resolvers.Query[entityName.toLowerCase()] = async (_: any, { id }: { id: string }) => { return entityRepo.findById(id); }; resolvers.Query[pluralName] = async () => { return entityRepo.find(); }; // Add Mutation resolvers resolvers.Mutation[`create${entityName}`] = async (_: any, { input }: { input: any }) => { const entity = new (entityClass as any)(); // Copy input properties to entity Object.assign(entity, input); // Handle relationship IDs await handleRelationshipIds(entity, input, registry, repositories); // Save entity await entityRepo.save(entity); return entity; }; resolvers.Mutation[`update${entityName}`] = async (_: any, { id, input }: { id: string, input: any }) => { const entity = await entityRepo.findById(id); if (!entity) return null; // Copy input properties to entity Object.assign(entity, input); // Handle relationship IDs await handleRelationshipIds(entity, input, registry, repositories); // Save entity await entityRepo.save(entity); return entity; }; resolvers.Mutation[`delete${entityName}`] = async (_: any, { id }: { id: string }) => { await entityRepo.delete(id); return true; }; // Add field resolvers for relationships resolvers[entityName] = {}; const relationships = registry.getAllRelationships(entityClass); if (relationships) { for (const [relName, relOptions] of relationships.entries()) { const targetType = relOptions.target.name; const targetRepo = repositories[targetType]; if (!targetRepo) { console.warn(`No repository found for entity ${targetType}`); continue; } // Create resolver for this relationship resolvers[entityName][relName] = async (parent: any) => { if (!parent[relName]) { if (relOptions.cardinality === 'one') { // Load the related entity const criteria = { id: parent[`${relName}Id`] }; return parent[`${relName}Id`] ? targetRepo.findOne(criteria) : null; } else { // Load all related entities if (!parent[`${relName}Ids`] || parent[`${relName}Ids`].length === 0) { return []; } const criteria = { id: { $in: parent[`${relName}Ids`] } }; return targetRepo.find(criteria); } } return parent[relName]; }; } } } return resolvers; } /** * Handle relationship IDs in input objects */ async function handleRelationshipIds( entity: any, input: any, registry: MetadataRegistry, repositories: Record<string, Repository<any>> ) { const entityClass = entity.constructor; const relationships = registry.getAllRelationships(entityClass); if (!relationships) return; for (const [relName, relOptions] of relationships.entries()) { const targetType = relOptions.target.name; const targetRepo = repositories[targetType]; if (!targetRepo) continue; if (relOptions.cardinality === 'one') { // Handle to-one relationship const relatedId = input[`${relName}Id`]; if (relatedId) { const relatedEntity = await targetRepo.findById(relatedId); if (relatedEntity) { entity[relName] = relatedEntity; } } } else { // Handle to-many relationship const relatedIds = input[`${relName}Ids`]; if (relatedIds && Array.isArray(relatedIds)) { const relatedEntities = []; for (const id of relatedIds) { const relatedEntity = await targetRepo.findById(id); if (relatedEntity) { relatedEntities.push(relatedEntity); } } entity[relName] = relatedEntities; } } } } // Run the example main().catch(error => { console.error("Error running GraphQL example:", error); });