@wearesage/schema
Version:
A flexible schema definition and validation system for TypeScript with multi-database support
413 lines (330 loc) ⢠12.6 kB
text/typescript
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);
});