Skip to main content

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_active field

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 lokasi
  • setLocationPolicies(userId, tenantId, locationId, policyKeys) — Set/replace policies
  • syncLocationPolicies(userId, tenantId, locationId) — Sync dari core_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 di core_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

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 DTO
    • toTreeStructure() - Transform flat list ke tree structure
    • buildTree() - 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 / - Create
  • GET / - List (dengan query params)
  • GET /tree - Tree structure
  • GET /search - Quick search
  • GET /:id - Get by ID
  • GET /:id/children - Get children
  • GET /:id/parents - Get parents
  • GET /:id/usage - Get usage stats
  • GET /:id/policies - Get location policies
  • PUT /:id/policies - Set location policies
  • POST /:id/policies/sync - Sync policies dari mappings
  • POST /validate-parent - Validate parent sebelum create/update
  • POST /validate-attributes - Validate attributes sesuai schema type
  • PUT /:id - Update
  • PATCH /:id/status - Toggle status
  • POST /:id/move - Move location
  • DELETE /:id - Soft delete
  • DELETE /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 categories
  • GET /location-types - List types
  • GET /location-types/:key - Detail type
  • GET /location-types/:key/attributes - Attributes schema

LTree Path Management

Path Generation

Create Location:

  1. Jika root (parent_id = null): Generate path baru 0001
  2. Jika ada parent: Concatenate parent path + new UUID segment
    • Parent path: 0001.0002
    • New path: 0001.0002.0003

Move Location:

  1. Validasi new parent
  2. Calculate new path berdasarkan new parent
  3. Update path untuk location
  4. 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_keys harus ada di core_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

CodeHTTPDescription
location.not-found404Lokasi tidak ditemukan
location.parent-not-found404Parent tidak ditemukan
location.parent-inactive400Parent tidak aktif
location.category-not-found404category_key tidak ada
location.type-not-found404location_type_key tidak ada
location.type-hierarchy-invalid400Type kind hierarchy tidak diizinkan
location.circular-reference-self400parent = id lokasi itu sendiri
location.circular-reference-descendant400Parent baru adalah descendant
location.code-not-unique400Code duplikat dalam org_unit
location.invalid-policy-keys400Policy key tidak valid
location.has-active-children400Ada child aktif (block deactivate/soft delete)
location.not-soft-deleted400Hard delete tanpa soft delete dulu
location.has-children400Ada 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 queries
  • idx_locations_org_unit - Filter by organization unit
  • idx_locations_parent - Parent lookups
  • idx_locations_code - Code uniqueness check

Query Optimization

  • Use select untuk 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 URL
  • AUTH_SECRET_KEY - Secret key untuk auth service
  • MY_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

  1. Bulk Operations

    • Bulk create locations
    • Bulk update
    • Bulk move
  2. Advanced Filtering

    • Filter by geographic bounds
    • Filter by attributes
    • Complex search queries
  3. Audit Trail

    • Track location changes
    • History of moves
    • Change logs
  4. Performance

    • Implement caching layer
    • Optimize tree queries
    • Add database connection pooling
  5. 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