digitaltwin-core
Version:
Minimalist framework to collect and handle data in a Digital Twin project
662 lines • 23 kB
TypeScript
import type { Component, Servable } from './interfaces.js';
import type { AssetsManagerConfiguration, DataResponse } from './types.js';
import type { HttpMethod } from '../engine/endpoints.js';
import type { StorageService } from '../storage/storage_service.js';
import type { DatabaseAdapter, MetadataRow } from '../database/database_adapter.js';
import type { DataRecord } from '../types/data_record.js';
import type { OpenAPIDocumentable, OpenAPIComponentSpec } from '../openapi/types.js';
import { UserService } from '../auth/user_service.js';
/**
* Extended metadata row for assets with additional fields.
* This will be stored as separate columns in the database table.
*
* @interface AssetMetadataRow
* @extends {MetadataRow}
*
* @example
* ```typescript
* const assetMeta: AssetMetadataRow = {
* name: 'gltf',
* type: 'model/gltf-binary',
* url: '/storage/gltf/model.glb',
* date: new Date(),
* description: '3D building model',
* source: 'https://example.com/data-source',
* owner_id: 123,
* filename: 'building.glb',
* is_public: true
* }
* ```
*/
export interface AssetMetadataRow extends MetadataRow {
/** Human-readable description of the asset */
description: string;
/** Source URL for data provenance and licensing compliance (must be valid URL) */
source: string;
/** ID of the user who owns this asset (for access control) */
owner_id: number | null;
/** Original filename provided by the user */
filename: string;
/** Whether the asset is publicly accessible (true) or private (false) */
is_public: boolean;
}
/**
* Request payload for creating a new asset.
*
* @interface CreateAssetRequest
*
* @example
* ```typescript
* const request: CreateAssetRequest = {
* description: '3D model of building',
* source: 'https://city-data.example.com/buildings',
* owner_id: 'user123',
* filename: 'building.glb',
* file: fileBuffer,
* is_public: true
* }
* ```
*/
export interface CreateAssetRequest {
/** Human-readable description of the asset */
description: string;
/** Source URL for data provenance (validated as proper URL) */
source: string;
/** Owner user ID for access control (can be null) */
owner_id: number | null;
/** Original filename */
filename: string;
/** File content as Buffer */
file: Buffer;
/** Whether the asset is publicly accessible (default: true) */
is_public?: boolean;
}
/**
* Request payload for updating asset metadata.
*
* @interface UpdateAssetRequest
*
* @example
* ```typescript
* const updates: UpdateAssetRequest = {
* description: 'Updated building model with new textures',
* source: 'https://updated-source.example.com',
* is_public: false
* }
* ```
*/
export interface UpdateAssetRequest {
/** Updated description (optional) */
description?: string;
/** Updated source URL (optional, validated if provided) */
source?: string;
/** Updated visibility (optional) */
is_public?: boolean;
}
/**
* Abstract base class for Assets Manager components with authentication and access control.
*
* Provides secure file upload, storage, and retrieval capabilities following the Digital Twin framework patterns.
* Each concrete implementation manages a specific type of asset and creates its own database table.
*
* ## Authentication & Authorization
*
* - **Write Operations** (POST, PUT, DELETE): Require authentication via Apache APISIX headers
* - **User Management**: Automatically creates/updates user records from Keycloak data
* - **Access Control**: Users can only modify/delete their own assets (ownership-based)
* - **Resource Linking**: Assets are automatically linked to their owners via user_id foreign key
*
* ## Required Headers for Authenticated Endpoints
*
* - `x-user-id`: Keycloak user UUID (required)
* - `x-user-roles`: Comma-separated list of user roles (optional)
*
* These headers are automatically added by Apache APISIX after successful Keycloak authentication.
*
* @abstract
* @class AssetsManager
* @implements {Component}
* @implements {Servable}
*
* @example
* ```typescript
* // Create concrete implementations for different asset types
* class GLTFAssetsManager extends AssetsManager {
* getConfiguration() {
* return { name: 'gltf', description: 'GLTF 3D models manager', ... }
* }
* }
*
* class PointCloudAssetsManager extends AssetsManager {
* getConfiguration() {
* return { name: 'pointcloud', description: 'Point cloud data manager', ... }
* }
* }
*
* // Usage in engine
* const gltfManager = new GLTFAssetsManager()
* gltfManager.setDependencies(database, storage)
*
* // Each creates its own table and endpoints:
* // - GLTFAssetsManager → table 'gltf', endpoints /gltf/*
* // - PointCloudAssetsManager → table 'pointcloud', endpoints /pointcloud/*
* ```
*
* @remarks
* Asset metadata is stored as dedicated columns in the database table:
* - id, name, url, date (standard columns)
* - description, source, owner_id, filename (asset-specific columns)
*
* Each concrete AssetsManager creates its own table based on the configuration name.
*/
export declare abstract class AssetsManager implements Component, Servable, OpenAPIDocumentable {
protected db: DatabaseAdapter;
protected storage: StorageService;
protected userService: UserService;
/**
* Injects dependencies into the assets manager.
*
* Called by the framework during component initialization.
*
* @param {DatabaseAdapter} db - The database adapter for metadata storage
* @param {StorageService} storage - The storage service for file persistence
* @param {UserService} [userService] - Optional user service for authentication (created automatically if not provided)
*
* @example
* ```typescript
* // Standard usage (UserService created automatically)
* const assetsManager = new MyAssetsManager()
* assetsManager.setDependencies(databaseAdapter, storageService)
*
* // For testing (inject mock UserService)
* const mockUserService = new MockUserService()
* assetsManager.setDependencies(databaseAdapter, storageService, mockUserService)
* ```
*/
setDependencies(db: DatabaseAdapter, storage: StorageService, userService?: UserService): void;
/**
* Returns the configuration of the assets manager.
*
* Must be implemented by subclasses to define the asset type,
* table name, and content types.
*
* @abstract
* @returns {ComponentConfiguration} The component configuration
*
* @example
* ```typescript
* class GLTFAssetsManager extends AssetsManager {
* getConfiguration(): ComponentConfiguration {
* return {
* name: 'gltf',
* description: 'GLTF 3D models manager',
* contentType: 'model/gltf-binary',
* tags: ['assets', '3d', 'gltf']
* }
* }
* }
* ```
*/
abstract getConfiguration(): AssetsManagerConfiguration;
/**
* Validates that a source string is a valid URL.
*
* Used internally to ensure data provenance URLs are properly formatted.
*
* @private
* @param {string} source - The source URL to validate
* @returns {boolean} True if the source is a valid URL, false otherwise
*
* @example
* ```typescript
* this.validateSourceURL('https://example.com/data') // returns true
* this.validateSourceURL('not-a-url') // returns false
* ```
*/
private validateSourceURL;
/**
* Validates that a filename has the correct extension as configured.
*
* Used internally to ensure uploaded files match the expected extension.
*
* @private
* @param {string} filename - The filename to validate
* @returns {boolean} True if the filename has the correct extension or no extension is configured, false otherwise
*
* @example
* ```typescript
* // If config.extension = '.glb'
* this.validateFileExtension('model.glb') // returns true
* this.validateFileExtension('model.json') // returns false
* this.validateFileExtension('model') // returns false
*
* // If config.extension is undefined
* this.validateFileExtension('any-file.ext') // returns true
* ```
*/
private validateFileExtension;
/**
* Validates that a string is valid base64-encoded data.
*
* Used internally to ensure file data in batch uploads is properly base64-encoded
* before attempting to decode it.
*
* @private
* @param {any} data - Data to validate as base64
* @returns {boolean} True if data is a valid base64 string, false otherwise
*
* @example
* ```typescript
* this.validateBase64('SGVsbG8gV29ybGQ=') // returns true
* this.validateBase64('not-base64!@#') // returns false
* this.validateBase64(123) // returns false (not a string)
* this.validateBase64('') // returns false (empty string)
* ```
*/
private validateBase64;
/**
* Authenticates a request and returns the user record.
*
* This method consolidates the authentication flow:
* 1. Validates APISIX headers are present
* 2. Parses authentication headers
* 3. Finds or creates user record in database
*
* @param req - HTTP request object
* @returns AuthResult with either userRecord on success or DataResponse on failure
*
* @example
* ```typescript
* const authResult = await this.authenticateRequest(req)
* if (!authResult.success) {
* return authResult.response
* }
* const userRecord = authResult.userRecord
* ```
*/
private authenticateRequest;
/**
* Extracts upload data from multipart form request.
*
* @param req - HTTP request object with body and file
* @returns UploadData object with extracted fields
*/
private extractUploadData;
/**
* Validates required fields for asset upload and returns validated data.
*
* @param data - Upload data to validate
* @returns UploadValidationResult with validated data on success or error response on failure
*/
private validateUploadFields;
/**
* Reads file content from temporary upload path.
*
* @param filePath - Path to temporary file
* @returns Buffer with file content
* @throws Error if file cannot be read
*/
private readTempFile;
/**
* Cleans up temporary file after processing.
* Silently ignores cleanup errors.
*
* @param filePath - Path to temporary file
*/
private cleanupTempFile;
/**
* Validates ownership of an asset.
*
* Admins can modify any asset. Regular users can only modify their own assets
* or assets with no owner (owner_id = null).
*
* @param asset - Asset record to check
* @param userId - User ID to validate against
* @param headers - HTTP request headers (optional, for admin check)
* @returns DataResponse with error if not owner/admin, undefined if valid
*/
private validateOwnership;
/**
* Checks if a user can access a private asset.
*
* @param asset - Asset record to check
* @param req - HTTP request for authentication context
* @returns DataResponse with error if access denied, undefined if allowed
*/
private checkPrivateAssetAccess;
/**
* Fetches an asset by ID with full access control validation.
*
* This method consolidates the common logic for retrieving an asset:
* 1. Validates that ID is provided
* 2. Fetches the asset from database
* 3. Verifies the asset belongs to this component
* 4. Checks access permissions for private assets
*
* @param req - HTTP request with params.id
* @returns Object with asset on success, or DataResponse on failure
*/
private fetchAssetWithAccessCheck;
/**
* Upload a new asset file with metadata.
*
* Stores the file using the storage service and saves metadata to the database.
* Asset metadata is stored as dedicated columns in the database table.
*
* @param {CreateAssetRequest} request - The asset upload request
* @throws {Error} If source URL is invalid
*
* @example
* ```typescript
* await assetsManager.uploadAsset({
* description: '3D building model',
* source: 'https://city-data.example.com/buildings',
* owner_id: 'user123',
* filename: 'building.glb',
* file: fileBuffer,
* is_public: true
* })
* ```
*/
uploadAsset(request: CreateAssetRequest): Promise<void>;
/**
* Retrieve all assets for this component (like other components).
*
* Returns a JSON list of all assets with their metadata, following the
* framework pattern but adapted for assets management.
*
* Access control:
* - Unauthenticated users: Can only see public assets
* - Authenticated users: Can see public assets + their own private assets
* - Admin users: Can see all assets (public and private from all users)
*
* @returns {Promise<DataResponse>} JSON response with all assets
*/
retrieve(req?: any): Promise<DataResponse>;
/**
* Gets the authenticated user's database ID from request headers.
*
* @param req - HTTP request object
* @returns User ID or null if not authenticated
*/
private getAuthenticatedUserId;
/**
* Formats assets for API response with metadata and URLs.
*
* @param assets - Array of asset records
* @returns Formatted assets array ready for JSON serialization
*/
private formatAssetsForResponse;
/**
* Get all assets for this component type.
*
* Retrieves all assets managed by this component, with their metadata.
* Uses a very old start date to get all records.
*
* @returns {Promise<DataRecord[]>} Array of all asset records
*
* @example
* ```typescript
* const allAssets = await assetsManager.getAllAssets()
* // Returns: [{ id, name, type, url, date, contentType }, ...]
* ```
*/
getAllAssets(): Promise<DataRecord[]>;
/**
* Get asset by specific ID.
*
* @param {string} id - The asset ID to retrieve
* @returns {Promise<DataRecord | undefined>} The asset record or undefined if not found
*
* @example
* ```typescript
* const asset = await assetsManager.getAssetById('123')
* if (asset) {
* const fileData = await asset.data()
* }
* ```
*/
getAssetById(id: string): Promise<DataRecord | undefined>;
/**
* Update asset metadata by ID.
*
* Updates the metadata (description, source, and/or visibility) of a specific asset.
* Asset metadata is stored as dedicated columns in the database.
*
* @param {string} id - The ID of the asset to update
* @param {UpdateAssetRequest} updates - The metadata updates to apply
* @throws {Error} If source URL is invalid or asset not found
*
* @example
* ```typescript
* await assetsManager.updateAssetMetadata('123', {
* description: 'Updated building model with new textures',
* source: 'https://updated-source.example.com',
* is_public: false
* })
* ```
*/
updateAssetMetadata(id: string, updates: UpdateAssetRequest): Promise<void>;
/**
* Delete asset by ID.
*
* Removes a specific asset.
*
* @param {string} id - The ID of the asset to delete
* @throws {Error} If asset not found or doesn't belong to this component
*
* @example
* ```typescript
* await assetsManager.deleteAssetById('123')
* ```
*/
deleteAssetById(id: string): Promise<void>;
/**
* Delete latest asset (simplified)
*
* Removes the most recently uploaded asset for this component type.
*
* @throws {Error} If no assets exist to delete
*
* @example
* ```typescript
* await assetsManager.deleteLatestAsset()
* ```
*/
deleteLatestAsset(): Promise<void>;
/**
* Upload multiple assets in batch for better performance
*
* @param {CreateAssetRequest[]} requests - Array of asset upload requests
* @throws {Error} If any source URL is invalid
*
* @example
* ```typescript
* await assetsManager.uploadAssetsBatch([
* { description: 'Model 1', source: 'https://example.com/1', file: buffer1, ... },
* { description: 'Model 2', source: 'https://example.com/2', file: buffer2, ... }
* ])
* ```
*/
uploadAssetsBatch(requests: CreateAssetRequest[]): Promise<void>;
/**
* Delete multiple assets by IDs in batch
*
* @param {string[]} ids - Array of asset IDs to delete
* @throws {Error} If any asset not found or doesn't belong to this component
*/
deleteAssetsBatch(ids: string[]): Promise<void>;
/**
* Get endpoints following the framework pattern
*/
/**
* Get HTTP endpoints exposed by this assets manager.
*
* Returns the standard CRUD endpoints following the framework pattern.
*
* @returns {Array} Array of endpoint descriptors with methods, paths, and handlers
*
* @example
* ```typescript
* // For a manager with assetType: 'gltf', provides:
* GET /gltf - Get all assets
* POST /gltf/upload - Upload new asset
* GET /gltf/123 - Get specific asset
* PUT /gltf/123 - Update asset metadata
* GET /gltf/123/download - Download asset
* DELETE /gltf/123 - Delete asset
* ```
*/
getEndpoints(): Array<{
method: HttpMethod;
path: string;
handler: (...args: any[]) => any;
responseType?: string;
}>;
/**
* Returns the OpenAPI specification for this assets manager's endpoints.
*
* Generates documentation for all CRUD endpoints including batch operations.
* Can be overridden by subclasses for more detailed specifications.
*
* @returns {OpenAPIComponentSpec} OpenAPI paths, tags, and schemas for this assets manager
*/
getOpenAPISpec(): OpenAPIComponentSpec;
/**
* Handle single asset upload via HTTP POST.
*
* Flow:
* 1. Validate request structure and authentication
* 2. Extract user identity from Apache APISIX headers
* 3. Validate file extension and read uploaded file
* 4. Store file via storage service and metadata in database
* 5. Set owner_id to authenticated user (prevents ownership spoofing)
* 6. Apply is_public setting (defaults to true if not specified)
*
* Authentication: Required
* Ownership: Automatically set to authenticated user
*
* @param req - HTTP request with multipart/form-data file upload
* @returns HTTP response with success/error status
*
* @example
* POST /assets
* Content-Type: multipart/form-data
* x-user-id: user-uuid
* x-user-roles: user,premium
*
* Form data:
* - file: <binary file>
* - description: "3D model of building"
* - source: "https://source.com"
* - is_public: true
*/
handleUpload(req: any): Promise<DataResponse>;
/**
* Handle update endpoint (PUT).
*
* Updates metadata for a specific asset by ID.
*
* @param {any} req - HTTP request object with params.id and body containing updates
* @returns {Promise<DataResponse>} HTTP response
*
* @example
* ```typescript
* // PUT /gltf/123
* // Body: { "description": "Updated model", "source": "https://new-source.com" }
* ```
*/
handleUpdate(req: any): Promise<DataResponse>;
/**
* Handle get asset endpoint (GET).
*
* Returns the file content of a specific asset by ID for display/use in front-end.
* No download headers - just the raw file content.
*
* Access control:
* - Public assets: Accessible to everyone
* - Private assets: Accessible only to owner
* - Admin users: Can access all assets (public and private)
*
* @param {any} req - HTTP request object with params.id
* @returns {Promise<DataResponse>} HTTP response with file content
*
* @example
* ```typescript
* // GET /gltf/123
* // Returns the .glb file content for display in 3D viewer
* ```
*/
handleGetAsset(req: any): Promise<DataResponse>;
/**
* Handle download endpoint (GET).
*
* Downloads the file content of a specific asset by ID with download headers.
* Forces browser to download the file rather than display it.
*
* Access control:
* - Public assets: Accessible to everyone
* - Private assets: Accessible only to owner
* - Admin users: Can download all assets (public and private)
*
* @param {any} req - HTTP request object with params.id
* @returns {Promise<DataResponse>} HTTP response with file content and download headers
*
* @example
* ```typescript
* // GET /gltf/123/download
* // Returns the .glb file with download headers - browser will save it
* ```
*/
handleDownload(req: any): Promise<DataResponse>;
/**
* Handle delete endpoint (DELETE).
*
* Deletes a specific asset by ID.
*
* @param {any} req - HTTP request object with params.id
* @returns {Promise<DataResponse>} HTTP response
*
* @example
* ```typescript
* // DELETE /gltf/123
* ```
*/
handleDelete(req: any): Promise<DataResponse>;
/**
* Handle batch upload endpoint
*/
handleUploadBatch(req: any): Promise<DataResponse>;
/**
* Validates all requests in a batch upload.
*
* @param requests - Array of upload requests to validate
* @returns DataResponse with error if validation fails, undefined if valid
*/
private validateBatchRequests;
/**
* Processes batch upload requests.
*
* @param requests - Array of upload requests
* @param ownerId - Owner user ID
* @returns Array of results for each upload
*/
private processBatchUploads;
/**
* Handle batch delete endpoint
*/
handleDeleteBatch(req: any): Promise<DataResponse>;
/**
* Processes batch delete requests.
*
* Admins can delete any asset. Regular users can only delete their own assets
* or assets with no owner.
*
* @param ids - Array of asset IDs to delete
* @param userId - User ID for ownership validation
* @param headers - HTTP request headers (for admin check)
* @returns Array of results for each deletion
*/
private processBatchDeletes;
}
//# sourceMappingURL=assets_manager.d.ts.map