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​