npm package: @idrall/crud-core
Description: Generic CRUD core functionality for iDrall ERP microservices
Author: Idrall Development Team
Latest version: 1.1.4

# @idrall/crud-core

Generic CRUD core functionality for iDrall ERP microservices built with NestJS, Kysely, and PostgreSQL.

## Features

- 🚀 **Generic CRUD Operations**: List, Get, Create, Update, Delete operations with filtering, sorting, and pagination
- 🔧 **Configurable**: Per-entity configuration with table mapping and field exclusion
- 🎣 **Lifecycle Hooks**: Pre/post hooks for create, update, and delete operations
- 🔍 **Advanced Filtering**: Enhanced query filter system with validation and sanitization
- 📨 **Message Pattern Support**: Built-in NestJS microservice message pattern handling
- 🛡️ **Type Safe**: Full TypeScript support with comprehensive type definitions
- 🏗️ **Modular Architecture**: Dynamic module system with configuration support

## Installation

```bash
npm install @idrall/crud-core
```

### Peer Dependencies

Make sure you have the following peer dependencies installed:

```bash
npm install @nestjs/common @nestjs/core @nestjs/microservices kysely reflect-metadata rxjs
```

## Quick Start

### 1. Configure the Module

```typescript
import { Module } from "@nestjs/common";
import { DynamicCrudModule, CrudConfig } from "@idrall/crud-core";
import { KyselyModule } from "./kysely/kysely.module";

const crudConfig: CrudConfig = {
  serviceName: "idrall-crm",
  entities: ["customer", "contact", "opportunity", "lead", "campaign"],
  defaultTableMapping: {
    opportunity: "crm_opportunity",
  },
  entityConfigs: {
    contact: {
      excludeFields: ["password_hash"],
      hooks: {
        preCreate: async (entity, data, user) => {
          // Email validation hook
          if (
            data.email &&
            !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email as string)
          ) {
            throw new Error("Invalid email format");
          }
          return data;
        },
      },
    },
  },
};

@Module({
  imports: [KyselyModule, DynamicCrudModule.forRoot(crudConfig)],
})
export class AppModule {}
```

### 2. Provide KyselyService

Create a provider for the KyselyService:

```typescript
import { Module } from "@nestjs/common";
import { KyselyService } from "./kysely.service";

@Module({
  providers: [
    KyselyService,
    {
      provide: "KYSELY_SERVICE",
      useExisting: KyselyService,
    },
  ],
  exports: ["KYSELY_SERVICE"],
})
export class KyselyModule {}
```

### 3. Use Message Patterns

Your microservice will automatically handle these message patterns:

- `idrall-crm.customer.list` - List customers with filtering/pagination
- `idrall-crm.customer.get` - Get a single customer by ID
- `idrall-crm.customer.create` - Create a new customer
- `idrall-crm.customer.update` - Update an existing customer
- `idrall-crm.customer.delete` - Delete a customer

## Configuration

### CrudConfig

```typescript
interface CrudConfig {
  serviceName: string; // Microservice name
  entities: string[]; // Supported entities
  entityConfigs?: Record<string, EntityConfig>; // Per-entity config
  defaultTableMapping?: Record<string, string>; // Entity to table mapping
  globalHooks?: {
    // Global lifecycle hooks
    preCreate?: PreCreateHook;
    preUpdate?: PreUpdateHook;
    postCreate?: PostCreateHook;
    postUpdate?: PostUpdateHook;
    preDelete?: PreDeleteHook;
    postDelete?: PostDeleteHook;
  };
}
```

### EntityConfig

```typescript
interface EntityConfig {
  tableName?: string; // Override table name
  excludeFields?: string[]; // Fields to exclude from responses
  hooks?: {
    // Entity-specific hooks
    preCreate?: PreCreateHook;
    preUpdate?: PreUpdateHook;
    postCreate?: PostCreateHook;
    postUpdate?: PostUpdateHook;
    preDelete?: PreDeleteHook;
    postDelete?: PostDeleteHook;
  };
}
```

## Lifecycle Hooks

Hooks allow you to customize behavior at different points in the CRUD lifecycle:

```typescript
const config: CrudConfig = {
  serviceName: "my-service",
  entities: ["user"],
  entityConfigs: {
    user: {
      hooks: {
        preCreate: async (entity, data, user) => {
          // Hash password before creating user
          if (data.password) {
            data.password_hash = await hashPassword(data.password);
            delete data.password;
          }
          return data;
        },

        preUpdate: async (entity, id, data, user) => {
          // Validate email format
          if (
            data.email &&
            !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email as string)
          ) {
            throw new ValidationError("Invalid email format");
          }
          return data;
        },

        postCreate: async (entity, result, user) => {
          // Send welcome email after user creation
          await sendWelcomeEmail(result.email);
          return result;
        },
      },
    },
  },
};
```

## Query Filtering

The package includes advanced filtering capabilities:

```typescript
import { QueryFilter } from "@idrall/crud-core";

const filters: QueryFilter[] = [
  { field: "name", operator: "contains", value: "John" },
  { field: "age", operator: "ge", value: 18 },
  { field: "status", operator: "in", value: ["active", "pending"] },
];
```

### Supported Operators

- `eq` - Equal
- `ne` - Not equal
- `gt` - Greater than
- `lt` - Less than
- `ge` - Greater than or equal
- `le` - Less than or equal
- `contains` - String contains (case-insensitive)
- `startswith` - String starts with (case-insensitive)
- `endswith` - String ends with (case-insensitive)
- `in` - Value in array

## Usage from Gateway

Send requests to your CRM microservice from the gateway:

```typescript
import { Injectable, Inject } from "@nestjs/common";
import { ClientProxy } from "@nestjs/microservices";
import { lastValueFrom, timeout } from "rxjs";

@Injectable()
export class CustomerGatewayService {
  constructor(@Inject("NATS_SERVICE") private client: ClientProxy) {}

  async listCustomers(filters: any[] = [], pagination?: any) {
    const payload = {
      entity: "customer",
      filters,
      sort: [{ field: "created_at", order: "desc" }],
      pagination: pagination || { skip: 0, limit: 20 },
    };

    return lastValueFrom(
      this.client.send("idrall-crm.customer.list", payload).pipe(timeout(10000))
    );
  }

  async createCustomer(data: any) {
    const payload = {
      entity: "customer",
      data,
    };

    return lastValueFrom(
      this.client
        .send("idrall-crm.customer.create", payload)
        .pipe(timeout(10000))
    );
  }
}
```

## Advanced Usage

### Async Configuration

```typescript
DynamicCrudModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: async (configService: ConfigService): Promise<CrudConfig> => {
    return {
      serviceName: configService.get("SERVICE_NAME"),
      entities: configService.get("SUPPORTED_ENTITIES").split(","),
      // ... other config
    };
  },
});
```

### Multiple Feature Modules

```typescript
@Module({
  imports: [
    DynamicCrudModule.forFeature({
      serviceName: "crm-contacts",
      entities: ["contact", "address"],
    }),
    DynamicCrudModule.forFeature({
      serviceName: "crm-sales",
      entities: ["opportunity", "quote"],
    }),
  ],
})
export class CrmModule {}
```

## Error Handling

The package includes custom error types:

```typescript
import {
  CrudError,
  ValidationError,
  EntityNotFoundError,
  UnauthorizedError,
} from "@idrall/crud-core";

// These errors are automatically handled by the controller
// and converted to appropriate response formats
```

## Docker & AWS Fargate Optimization

The package is optimized for containerized deployments:

- Minimal dependencies to reduce image size
- Efficient connection pooling for PostgreSQL
- Proper error handling for distributed systems
- Health check endpoints included

## API Reference

### DynamicCrudService

- `list<T>(query: DynamicQuery): Promise<ListResponse<T>>`
- `getOne<T>(entity: string, id: string | number): Promise<T | null>`
- `create<T>(entity: string, data: Record<string, unknown>, user?: any): Promise<T>`
- `update<T>(entity: string, id: string | number, data: Record<string, unknown>, user?: any): Promise<T>`
- `delete<T>(entity: string, id: string | number, user?: any): Promise<T>`

### DynamicCrudController

Automatically handles message patterns:

- `*.*.list` - List entities
- `*.*.get` - Get single entity
- `*.*.create` - Create entity
- `*.*.update` - Update entity
- `*.*.delete` - Delete entity
- `*.*.bulk-create` - Bulk create
- `*.*.bulk-update` - Bulk update
- `*.*.bulk-delete` - Bulk delete
- `*.*.count` - Count entities
- `*.health` - Health check

## Contributing

1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## Support

For support, please contact the Idrall Development Team or create an issue in the repository.

## Entity Registry Integration (NEW)

You can now pass your entity registry metadata/config as a third argument to `DynamicCrudModule.forRoot`. This allows you to centralize and share your entity definitions across your microservices, and makes the metadata available as a provider (`ENTITY_REGISTRY_CONFIG`).

### Example Usage

```typescript
import { DynamicCrudModule, CrudConfig } from "@idrall/crud-core";
import { KyselyModule } from "./kysely/kysely.module";
import { entityRegistryConfig } from "./config/entity-registry";

const crudConfig: CrudConfig = {
  serviceName: "idrall-crm",
  entities: [
    "agent",
    "sales_team",
    // ...
  ],
  // ...
};

@Module({
  imports: [
    DynamicCrudModule.forRoot(crudConfig, KyselyModule, entityRegistryConfig),
    // ...
  ],
})
export class AppModule {}
```

### How to Inject the Metadata

You can inject the entity registry config anywhere in your app:

```typescript
import { Inject, Injectable } from "@nestjs/common";

@Injectable()
export class MyService {
  constructor(
    @Inject("ENTITY_REGISTRY_CONFIG") private readonly entityRegistry: any
  ) {}

  getEntityList() {
    return this.entityRegistry.entities;
  }
}
```

---

## Difference between `crudConfig` and `entityRegistryConfig`

- **crudConfig**: This is the configuration object used by the CRUD engine itself. It tells the CRUD module which entities to expose, how to map them to tables, what hooks to use, and what fields to exclude. It is focused on the runtime CRUD logic and how your API behaves.

- **entityRegistryConfig**: This is a metadata object that describes your entities in a more detailed and structured way. It is typically used for sharing entity definitions across services, for dynamic API generation, validation, documentation, and for registering your entities in a central registry microservice. It usually contains field types, relationships, permissions, and more. It is not required for CRUD to work, but is very useful for advanced scenarios and for interoperability between services.

**In summary:**

- Use `crudConfig` to control CRUD behavior and mapping.
- Use `entityRegistryConfig` to describe your entities for registry, validation, and cross-service metadata sharing.