UNPKG

digitaltwin-core

Version:

Minimalist framework to collect and handle data in a Digital Twin project

662 lines 23 kB
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