Dokumentasi Teknis
Overview
Dokumentasi teknis untuk developer yang akan maintain atau develop lebih lanjut pada Locations API module.
Arsitektur
Locations module menggunakan CQRS pattern dengan struktur sebagai berikut:
locations/
├── commands/ # Command handlers untuk write operations
├── queries/ # Query handlers untuk read operations
├── dto/ # Data Transfer Objects
├── mappers/ # Data mappers untuk transformasi data
├── validation/ # Custom validators
├── locations.controller.ts
├── locations.service.ts
├── locations.module.ts
└── docs/ # Dokumentasi
Tech Stack
- Framework: NestJS
- Database: PostgreSQL dengan Prisma ORM
- Hierarchy: PostgreSQL LTree extension untuk path management
- Validation: class-validator, class-transformer
- API Documentation: Swagger/OpenAPI
Database Schema
Table: locations
CREATE TABLE locations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
org_unit_id UUID NOT NULL REFERENCES organization_units(id),
op_unit_id UUID REFERENCES operational_units(id),
location_type_key VARCHAR NOT NULL,
parent_location_id UUID REFERENCES locations(id),
name VARCHAR NOT NULL,
short_name VARCHAR,
slug VARCHAR,
code VARCHAR NOT NULL,
category_key VARCHAR NOT NULL,
path_ltree LTREE, -- PostgreSQL LTree untuk hierarchy
is_active BOOLEAN DEFAULT true,
address TEXT,
latitude DECIMAL(10, 8),
longitude DECIMAL(11, 8),
attributes JSONB,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
deleted_at TIMESTAMP,
-- Constraints
UNIQUE(org_unit_id, code), -- Code unique per organization unit
CHECK (latitude BETWEEN -90 AND 90),
CHECK (longitude BETWEEN -180 AND 180)
);
-- Indexes
CREATE INDEX idx_locations_org_unit ON locations(org_unit_id);
CREATE INDEX idx_locations_parent ON locations(parent_location_id);
CREATE INDEX idx_locations_path_ltree ON locations USING GIST(path_ltree);
CREATE INDEX idx_locations_type ON locations(location_type_key);
CREATE INDEX idx_locations_code ON locations(org_unit_id, code);
LTree Path Structure
LTree digunakan untuk menyimpan path hierarkis lokasi. Format:
- Root location:
0001(atau UUID segment pertama) - Child location:
0001.0002(parent path + UUID segment) - Deep hierarchy:
0001.0002.0003.0004
Keuntungan LTree:
- Query cepat untuk descendants/ancestors
- Built-in PostgreSQL support untuk hierarchy queries
- Index GIST untuk performa optimal
Module Structure
1. Commands (Write Operations)
Semua write operations menggunakan Command pattern:
CreateLocationCommand
- File:
commands/create-location.command.ts - Handler:
LocationsService.create() - Validations:
- Organization unit exists
- Location type exists
- Parent exists and active (if provided)
- Type hierarchy validation
- Code uniqueness within org_unit
- LTree Path: Auto-generated berdasarkan parent path
UpdateLocationCommand
- File:
commands/update-location.command.ts - Handler:
LocationsService.update() - Validations:
- Circular reference check
- Type hierarchy re-validation
- Code uniqueness check
- LTree Path: Recalculated jika parent berubah
MoveLocationCommand
- File:
commands/move-location.command.ts - Handler:
LocationsService.move() - Validations:
- New parent exists and active
- Cannot move to self
- Cannot move to descendant
- Type hierarchy validation
- LTree Path: Recalculated untuk location dan semua descendants
ToggleStatusCommand
- File:
commands/toggle-status.command.ts - Handler:
LocationsService.toggleStatus() - Validations:
- Cannot deactivate if has active children
- Action: Toggle
is_activefield
SoftDeleteLocationCommand
- File:
commands/soft-delete-location.command.ts - Handler:
LocationsService.remove() - Validations:
- Cannot delete if has active children
- Cannot delete if has inventory items
- Action: Set
is_active = false
HardDeleteLocationCommand
- File:
commands/hard-delete-location.command.ts - Handler:
LocationsService.hardDelete() - Validations:
- Cannot delete if has any children
- Must be soft deleted first
- Action: Permanent delete dari database
2. Queries (Read Operations)
Semua read operations menggunakan Query pattern:
GetLocationsQuery
- File:
queries/get-locations.query.ts - Handler:
LocationsService.findAll() - Features:
- Filtering (org_unit, type, category, parent, status)
- Search (name, code, short_name)
- Pagination
- Sorting
- Tree structure mode (
table_tree=1)
GetLocationByIdQuery
- File:
queries/get-location-by-id.query.ts - Handler:
LocationsService.findOne() - Returns: Full location details dengan relations
GetLocationChildrenQuery
- File:
queries/get-location-children.query.ts - Handler:
LocationsService.getChildren() - Returns: Direct children only (tidak recursive)
GetLocationParentsQuery
- File:
queries/get-location-parents.query.ts - Handler:
LocationsService.getParents() - Returns: All parents dari root sampai direct parent
SearchLocationQuery
- File:
queries/search-location.query.ts - Handler:
LocationsService.search() - Search Fields: name, code, short_name
- Returns: Quick search results dengan limit
GetLocationUsageQuery
- File:
queries/get-location-usage.query.ts - Handler:
LocationsService.getUsage() - Returns: Statistics (inventory count, child count)
Note: Policy operations logic ada di LocationsService (tidak pakai Query/Command terpisah):
getLocationPolicies(tenantId, locationId)— Get policies lokasisetLocationPolicies(userId, tenantId, locationId, policyKeys)— Set/replace policiessyncLocationPolicies(userId, tenantId, locationId)— Sync daricore_location_policy_mappings
3. DTOs (Data Transfer Objects)
CreateLocationDto
- File:
dto/create-location.dto.ts - Required: org_unit_id, location_type_key, name, code, category_key, is_active
- Optional: op_unit_id, parent_location_id, short_name, slug, address, latitude, longitude, attributes
UpdateLocationDto
- File:
dto/update-location.dto.ts - All fields optional - Partial update support
QueryParamLocationListDto
- File:
dto/query-param-location-list.dto.ts - Query parameters untuk filtering, pagination, sorting
LocationDetailResponseDto
- File:
dto/location-response.dto.ts - Response format untuk detail location
LocationPaginatedResponseDto
- File:
dto/location-response.dto.ts - Response format untuk paginated list
LocationUsageResponseDto
- File:
dto/location-response.dto.ts - Response format untuk usage statistics
SetLocationPoliciesBodyDto, LocationPoliciesResponseDto
- File:
dto/location-policies.dto.ts - Set body:
{ policy_keys: string[] }— required, array min 1 max 100, semua key harus ada dicore_location_policy - Get response:
{ location_id, policies: [{ key, name, group_name }] }
4. Validators
LocationValidator
- File:
validation/location.validator.ts - Validations:
- Category validation (
core_location_categories) - Type validation (
core_location_types) - Type hierarchy check (
core_location_type_hierarchy_rule— type kind) - Parent validation (exists, active)
- Circular reference check (self, descendant)
- Code uniqueness dalam org_unit
- Category validation (
UuidExistsConstraint
- File:
validation/uuid-exists.validator.ts - Custom validator untuk check UUID exists di table tertentu
- Usage:
@UuidExists('table_name', 'field_name?')
5. Mappers
LocationMapper
- File:
mappers/location.mapper.ts - Functions:
toResponseDto()- Map Prisma model ke response DTOtoTreeStructure()- Transform flat list ke tree structurebuildTree()- Recursive tree building
Service Layer
LocationsService
Service utama yang mengkoordinasikan semua operations.
Dependencies:
- PrismaService (database access)
- LocationValidator (validation logic)
- Commands & Queries handlers
Key Methods:
// Write operations
create(userId: string, tenantId: string, dto: CreateLocationDto)
update(userId: string, tenantId: string, id: string, dto: UpdateLocationDto)
move(userId: string, tenantId: string, id: string, newParentId: string)
toggleStatus(userId: string, tenantId: string, id: string)
remove(userId: string, tenantId: string, id: string)
hardDelete(userId: string, tenantId: string, id: string)
// Read operations
findAll(tenantId: string, query: QueryParamLocationListDto)
findOne(tenantId: string, id: string)
getChildren(tenantId: string, id: string)
getParents(tenantId: string, id: string)
search(tenantId: string, keyword: string, limit?: number)
getUsage(tenantId: string, id: string)
// Policy operations
getLocationPolicies(tenantId: string, locationId: string)
setLocationPolicies(userId: string, tenantId: string, locationId: string, policyKeys: string[])
syncLocationPolicies(userId: string, tenantId: string, locationId: string)
Controller Layer
LocationsController
REST API controller dengan decorators:
@Controller('locations')- Base route@UseGuards(AuthGuard)- Authentication guard@ApiTags('locations')- Swagger tag@ApiBearerAuth()- Swagger auth
Endpoints:
POST /- CreateGET /- List (dengan query params)GET /tree- Tree structureGET /search- Quick searchGET /:id- Get by IDGET /:id/children- Get childrenGET /:id/parents- Get parentsGET /:id/usage- Get usage statsGET /:id/policies- Get location policiesPUT /:id/policies- Set location policiesPOST /:id/policies/sync- Sync policies dari mappingsPOST /validate-parent- Validate parent sebelum create/updatePOST /validate-attributes- Validate attributes sesuai schema typePUT /:id- UpdatePATCH /:id/status- Toggle statusPOST /:id/move- Move locationDELETE /:id- Soft deleteDELETE /hard-delete/:id- Hard delete
LocationsMetaController
Meta/referensi data (mounted di base API path):
GET /location-policies- List semua policies (core data)GET /location-categories- List categoriesGET /location-types- List typesGET /location-types/:key- Detail typeGET /location-types/:key/attributes- Attributes schema
LTree Path Management
Path Generation
Create Location:
- Jika root (parent_id = null): Generate path baru
0001 - Jika ada parent: Concatenate parent path + new UUID segment
- Parent path:
0001.0002 - New path:
0001.0002.0003
- Parent path:
Move Location:
- Validasi new parent
- Calculate new path berdasarkan new parent
- Update path untuk location
- Recalculate path untuk semua descendants menggunakan recursive query
LTree Queries
Get Children:
SELECT * FROM locations
WHERE path_ltree <@ (SELECT path_ltree FROM locations WHERE id = $1)
AND id != $1
AND path_ltree ~ (SELECT path_ltree::text || '.*{1}' FROM locations WHERE id = $1);
Get Parents:
SELECT * FROM locations
WHERE path_ltree @> (SELECT path_ltree FROM locations WHERE id = $1)
AND id != $1
ORDER BY nlevel(path_ltree);
Get Descendants:
SELECT * FROM locations
WHERE path_ltree <@ (SELECT path_ltree FROM locations WHERE id = $1)
AND id != $1;
Validation Rules
Referensi lengkap: VALIDATION-RULES.md
Category Validation
Rule: category_key harus ada di tabel core_location_categories.
Error: location.category-not-found (404)
Type Hierarchy
Relasi parent-child type diatur oleh core_location_type_hierarchy_rule (type kind, bukan level). Child type kind hanya boleh menjadi child dari parent type kind jika rule mengizinkan (allow = true).
Implementation: LocationValidator.validateHierarchy(client, parentType, childType)
Error: location.type-hierarchy-invalid (400), details: { parentTypeKind, childTypeKind }
Circular Reference Check
Self Reference:
if (locationId === parentId) {
throw new BadRequestException('Cannot set as own parent');
}
Descendant Check:
// Check if new parent is a descendant
const descendantPaths = await getDescendantPaths(locationId);
if (descendantPaths.includes(newParentPath)) {
throw new BadRequestException('Cannot set parent to descendant');
}
Code Uniqueness
Code harus unik dalam organization unit yang sama (hanya lokasi aktif).
Error: location.code-not-unique (400)
Set Location Policies
- Semua
policy_keysharus ada dicore_location_policy - Array: min 1, max 100 item
- Replace semantics (bukan append)
Error: location.invalid-policy-keys (400), details: { invalid_keys: string[] }
Error Handling
Error Response Format
{
success: false,
statusCode: number,
message: string,
reason: string, // Error code
details?: object,
path: string,
timestamp: string
}
Error Codes
| Code | HTTP | Description |
|---|---|---|
location.not-found | 404 | Lokasi tidak ditemukan |
location.parent-not-found | 404 | Parent tidak ditemukan |
location.parent-inactive | 400 | Parent tidak aktif |
location.category-not-found | 404 | category_key tidak ada |
location.type-not-found | 404 | location_type_key tidak ada |
location.type-hierarchy-invalid | 400 | Type kind hierarchy tidak diizinkan |
location.circular-reference-self | 400 | parent = id lokasi itu sendiri |
location.circular-reference-descendant | 400 | Parent baru adalah descendant |
location.code-not-unique | 400 | Code duplikat dalam org_unit |
location.invalid-policy-keys | 400 | Policy key tidak valid |
location.has-active-children | 400 | Ada child aktif (block deactivate/soft delete) |
location.not-soft-deleted | 400 | Hard delete tanpa soft delete dulu |
location.has-children | 400 | Ada children (block hard delete) |
Testing
Unit Tests
Test untuk setiap command dan query handler:
- Validation logic
- Business rules
- Error cases
Integration Tests
Test untuk:
- API endpoints
- Database operations
- LTree path management
- Hierarchy operations
Test Data
Setup test data dengan hierarchy:
Warehouse A (root)
└── Storage Area A1
└── Shelf A1-1
└── Bin A1-1-1
Performance Considerations
Indexes
idx_locations_path_ltree- GIST index untuk LTree queriesidx_locations_org_unit- Filter by organization unitidx_locations_parent- Parent lookupsidx_locations_code- Code uniqueness check
Query Optimization
- Use
selectuntuk limit fields - Pagination untuk large datasets
- Tree mode tidak menggunakan pagination (return all)
- Use LTree operators untuk efficient hierarchy queries
Caching
Consider caching untuk:
- Location type metadata
- Organization unit lookups
- Frequently accessed locations
Security
Authentication
Semua endpoints protected dengan AuthGuard:
- Validates JWT token via lania-sso
- Extracts user info dan tenant ID
- Attaches user data ke request
Authorization
Tenant isolation:
- Semua queries filter by
tenant_id - Users hanya bisa access locations dalam tenant mereka
- Cross-tenant access prevented
Input Validation
- DTO validation dengan class-validator
- UUID format validation
- Type checking
- SQL injection prevention (Prisma parameterized queries)
Deployment
Environment Variables
Required:
AUTH_URL- lania-sso service URLAUTH_SECRET_KEY- Secret key untuk auth serviceMY_SECRET_KEY- Service secret key- Database connection string
Database Migrations
LTree extension harus di-enable:
CREATE EXTENSION IF NOT EXISTS ltree;
Module Registration
Module sudah terdaftar di app.module.ts:
imports: [
// ...
LocationsModule,
]
Future Improvements
-
Bulk Operations
- Bulk create locations
- Bulk update
- Bulk move
-
Advanced Filtering
- Filter by geographic bounds
- Filter by attributes
- Complex search queries
-
Audit Trail
- Track location changes
- History of moves
- Change logs
-
Performance
- Implement caching layer
- Optimize tree queries
- Add database connection pooling
-
Features
- Location templates
- Import/Export locations
- Location cloning
- Batch operations
Troubleshooting
Common Issues
LTree path not updating:
- Check database triggers
- Verify parent_id changes
- Check LTree extension enabled
Circular reference errors:
- Verify validation logic
- Check LTree path calculations
- Review move operations
Performance issues:
- Check indexes
- Review query patterns
- Consider pagination
- Monitor database performance
Seeding
Location Policies
Core data policies di-seed dari prisma/seeds/seed-location-policies.ts:
- Source:
bahan/json/locataion_policies.json - Tables:
core_location_policy,core_location_policy_mappings - Run:
npm run seed(setelah seed location data)
User tidak bisa CRUD policies — data core dari sistem.
References
- VALIDATION-RULES.md — Aturan validasi lengkap per operasi
- locations.md — PRD & Business Requirements
- NestJS Documentation
- Prisma Documentation
- PostgreSQL LTree Extension
- CQRS Pattern