Documentation
Data access in Nitrokit
Documentation on the data access layer
Nitrokit provides a scalable Angular boilerplate for quickly shipping applications with integrated backend services, specifically leveraging Nx generators and Supabase. This documentation focuses on the crud-data-access-lib
generator, which generates two critical services for managing CRUD operations: the Entity Data Service and the Entity CRUD Data Service.
Why Are These Classes Generated?
The crud-data-access-lib
generator creates two classes:
- Entity Data Service (e.g.,
ProductDataService
) - Entity CRUD Data Service (e.g.,
ProductCrudDataService
)
1. Entity Data Service
The Entity Data Service is a lightweight class that extends the base Entity CRUD Data Service. This service is typically empty but allows for customization when needed.
Purpose:
- It provides a layer to override default CRUD methods from the CRUD data service.
- It allows the developer to add custom business logic without modifying the auto-generated code.
- It can be extended to perform specific data queries, filters, or even use more optimized fetching methods depending on the use case.
Example: ProductDataService
import { Injectable } from '@angular/core';
import { ProductCrudDataService } from './product-crud.data-service';
@Injectable({
providedIn: 'root',
})
export class ProductDataService extends ProductCrudDataService {}
Here, ProductDataService
is extending ProductCrudDataService
without adding any custom logic. This serves as a base for future overrides or specific custom data handling.
2. Entity CRUD Data Service
The Entity CRUD Data Service is where most of the logic resides. This service contains all CRUD operations that interact with Supabase. It is generated automatically based on the Nx generator configuration and is responsible for:
- Fetching, adding, updating, deleting, and counting items in the database.
- Automatically handling real-time changes from Supabase and updating accordingly.
- Enforcing basic access control using the
AuthService
(e.g., ensuring users can only interact with their data).
Purpose:
- This service should not be modified directly. Instead, it should be extended by the Entity Data Service to preserve the generated code for easier upgrades or re-generation.
- It ensures consistency and uniformity across your CRUD services while interacting with Supabase.
Example: ProductCrudDataService
import { inject, Injectable } from '@angular/core';
import { Database } from '@nitrokit/influencer-type-supabase';
import { APP_CONFIG } from '@nitrokit/shared-util-config';
import { createClient } from '@supabase/supabase-js';
import { Subject } from 'rxjs';
import { SupabaseFilter } from '@nitrokit/shared-type-supabase';
import { AuthService } from '@nitrokit/shared-data-access-auth';
import { SupabaseCrudService } from '@nitrokit/shared-type-supabase';
@Injectable({
providedIn: 'root',
})
export class ProductCrudDataService
implements SupabaseCrudService<Database, 'products'>
{
// Injected dependencies
private readonly appConfig = inject(APP_CONFIG);
protected readonly authService = inject(AuthService);
protected readonly supabase = createClient<Database>(
this.appConfig.supabaseUrl,
this.appConfig.supabaseKey
);
private readonly realtimeChange$$ = new Subject<void>();
// Observable for real-time changes
public readonly realtimeChange$ = this.realtimeChange$$.asObservable();
constructor() {
// Supabase real-time updates for the 'products' table
this.supabase
.channel('products')
.on('postgres_changes', { event: '*', schema: 'public', table: 'products' }, () => {
this.realtimeChange$$.next();
})
.subscribe();
}
// Various CRUD operations follow...
}
This service contains all CRUD operations like fetching paginated items, counting, and filtering. It automatically handles user-specific data (by checking profile_id
).
How to Use These Services
-
Inject the Data Service:
In your components, services, or effects, inject theProductDataService
to use it for CRUD operations. You typically don’t interact directly with the CRUD service unless you are extending or overriding it.import { Component, OnInit } from '@angular/core'; import { ProductDataService } from './product.data-service'; @Component({ selector: 'app-product-list', templateUrl: './product-list.component.html', }) export class ProductListComponent implements OnInit { constructor(private productDataService: ProductDataService) {} ngOnInit() { this.productDataService.getItems().then(products => { console.log(products); }); } }
-
Extend the Data Service:
If you need to customize queries or filters (e.g., fetch only certain fields, apply additional filters), you can override the methods inProductDataService
:import { Injectable } from '@angular/core'; import { ProductCrudDataService } from './product-crud.data-service'; @Injectable({ providedIn: 'root', }) export class ProductDataService extends ProductCrudDataService { // Override to fetch only 'name' and 'price' from the products public async getItems() { const userId = await this.authService.getUserId(); if (!userId) { throw new Error('Not authenticated'); } return this.supabase .from('products') .select('name, price') .filter('profile_id', 'eq', userId); } }
-
Real-time Data Handling:
The CRUD service automatically subscribes to real-time changes in Supabase for the relevant table. You can use therealtimeChange$
observable to trigger updates in your application (e.g., in an Angular Effect or Service):this.productDataService.realtimeChange$.subscribe(() => { this.loadProducts(); });
Generated Methods in CRUD Service
getPagedItems
: Fetches paginated products with optional search and sorting.getItems
: Fetches a list of products with optional filtering.count
: Returns the count of products based on filters.getItem
: Fetches a single product by its ID.add
: Adds a new product to the database.update
: Updates an existing product.delete
: Deletes a product by its ID.deleteRange
: Deletes multiple products by their IDs.
Benefits of Using Nx and Supabase Integration
- Type Safety: Using Nx generators ensures type safety across the application, especially when combined with Supabase's typed API for database interactions.
- Scalability: The generated services are structured to scale across different entities and maintainable for the long term.
- Real-Time Features: Supabase's real-time capabilities are automatically integrated into the CRUD service, allowing seamless synchronization of state changes without additional boilerplate.
- Customizability: The structure of the services allows for easy customizations, where developers can override specific behaviors, making it flexible for complex requirements.
Connecting with the state layer
Normally we will not use those data services directly in the components.
The crud-state-lib
generator will have created a state machine for us that automatically connects the generated service to the state machine.
Here is an example of an automatically generated state machine:
@Injectable({ providedIn: 'root' })
export class ProductStateMachine extends CrudRealtimeSupabaseEntityStateMachine<
Database,
'products'
> {
constructor(@Inject(ProductDataService) service: ProductDataService) {
super(service);
}
}
Conclusion
By utilizing the generated Entity Data Service and Entity CRUD Data Service,
you can rapidly build scalable Angular applications with Supabase, while maintaining full control over the customization of queries,
filters, and data fetching strategies.
These services enforce best practices, promote reusability, and embrace type safety, making it easier for developers to work efficiently.
Remember, the usage of the service is automatically tied to the state machines by the crud-state-lib
generator.
Have questions?
Still have questions? Talk to support.