/** * Container - Container entity for child entities * * A Container is a folder-like container that can hold child entities * of any type (Container, Document, Category, or Index). * * @version 0.1 * @since 0.1 */ namespace Implexus.Entities { /** * Container entity that can hold child entities. * * Containers are similar to filesystem folders - they can contain * any type of entity including other containers, documents, categories, * and indexes. * * Example usage: * {{{ * var root = yield engine.get_root_async(); * var users = yield root.create_container_async("users"); * var john = yield users.create_document_async("john", "User"); * yield john.set_entity_property_async("email", new Invercargill.NativeElement("john@example.com")); * }}} */ public class Container : AbstractEntity { // === Constructors === /** * Creates a new Container with the given engine and path. * * @param engine The engine that manages this entity * @param path The path to this container */ public Container(Core.Engine engine, Core.EntityPath path) { base(engine, path); } // === Entity Type === /** * {@inheritDoc} */ public override Core.EntityType entity_type { get { return Core.EntityType.CONTAINER; } } // === Child Navigation (Async) === /** * {@inheritDoc} * * Returns the names of all children stored in this container. */ public override async Invercargill.ReadOnlySet get_child_names_async() throws Core.EntityError { var storage = _engine.configuration.storage; try { var children = storage.get_children(_path); var set = new Invercargill.DataStructures.HashSet(); foreach (var name in children) { set.add(name); } return set; } catch (Storage.StorageError e) { throw new Core.EntityError.STORAGE_ERROR("Failed to get children: %s".printf(e.message)); } } /** * {@inheritDoc} * * Gets a child entity by name from storage. */ public override async Core.Entity? get_child_async(string name) throws Core.EntityError { var child_path = _path.append_child(name); return yield _engine.get_entity_async(child_path); } /** * {@inheritDoc} * * Returns all child entities (eager loading). */ public override async Core.Entity[] get_children_async() throws Core.EntityError { var names = yield get_child_names_async(); var children = new Core.Entity[0]; foreach (var name in names) { var child = yield get_child_async(name); if (child != null) { children += (!) child; } } return children; } // === Child Creation (Async) === /** * {@inheritDoc} * * Creates a new Container child in this container. * * @param name The name for the new container * @return The created container entity * @throws Core.EntityError.INVALID_PATH if name is empty * @throws Core.EntityError.ENTITY_ALREADY_EXISTS if an entity with this name already exists */ public override async Core.Entity? create_container_async(string name) throws Core.EntityError { validate_can_create_child(name); var child_path = _path.append_child(name); // Check if we're in a transaction var embedded_engine = _engine as Engine.EmbeddedEngine; var tx = embedded_engine != null ? embedded_engine.current_transaction : null; if (tx != null) { // Queue operations in transaction tx.record_create_entity(child_path, Core.EntityType.CONTAINER, null); tx.record_add_child(_path, name); } else { // Direct write when not in transaction var storage = _engine.configuration.storage; try { storage.store_entity_metadata(child_path, Core.EntityType.CONTAINER, null); storage.add_child(_path, name); } catch (Storage.StorageError e) { throw new Core.EntityError.STORAGE_ERROR("Failed to create container: %s".printf(e.message)); } } // Create entity instance and notify engine var container = new Container(_engine, child_path); _engine.entity_created(container); return container; } /** * {@inheritDoc} * * Creates a new Document child in this container. * * @param name The name for the new document * @param type_label The application-defined type for the document * @return The created document entity * @throws Core.EntityError.INVALID_PATH if name is empty * @throws Core.EntityError.ENTITY_ALREADY_EXISTS if an entity with this name already exists */ public override async Core.Entity? create_document_async(string name, string type_label) throws Core.EntityError { validate_can_create_child(name); var child_path = _path.append_child(name); // Check if we're in a transaction var embedded_engine = _engine as Engine.EmbeddedEngine; var tx = embedded_engine != null ? embedded_engine.current_transaction : null; if (tx != null) { // Queue operations in transaction tx.record_create_entity(child_path, Core.EntityType.DOCUMENT, type_label); tx.record_add_child(_path, name); // Note: CREATE_ENTITY for DOCUMENT already stores empty properties } else { // Direct write when not in transaction var storage = _engine.configuration.storage; try { storage.store_entity_metadata(child_path, Core.EntityType.DOCUMENT, type_label); storage.store_properties(child_path, new Invercargill.DataStructures.PropertyDictionary()); storage.add_child(_path, name); // Register document in type index for Index.populate_index() if (embedded_engine != null) { ((!) embedded_engine).entity_store.register_document_type(type_label, child_path.to_string()); } } catch (Storage.StorageError e) { throw new Core.EntityError.STORAGE_ERROR("Failed to create document: %s".printf(e.message)); } } // Create entity instance and notify engine var document = new Document(_engine, child_path, type_label); _engine.entity_created(document); return document; } /** * {@inheritDoc} * * Creates a new Category child in this container. * * The category will be populated with all existing documents that match * the predicate expression, and will automatically register for change * notifications to keep its index up to date. * * @param name The name for the new category * @param type_label The document type to filter * @param expression The boolean predicate expression for filtering * @return The created category entity * @throws Core.EntityError.INVALID_PATH if name is empty * @throws Core.EntityError.ENTITY_ALREADY_EXISTS if an entity with this name already exists */ public override async Core.Entity? create_category_async( string name, string type_label, string expression ) throws Core.EntityError { validate_can_create_child(name); var child_path = _path.append_child(name); // Check if we're in a transaction var embedded_engine = _engine as Engine.EmbeddedEngine; var tx = embedded_engine != null ? embedded_engine.current_transaction : null; if (tx != null) { // Queue operations in transaction tx.record_create_entity(child_path, Core.EntityType.CATEGORY, type_label); tx.record_save_category_config(child_path, type_label, expression); tx.record_add_child(_path, name); } else { // Direct write when not in transaction var storage = _engine.configuration.storage; try { storage.store_entity_metadata(child_path, Core.EntityType.CATEGORY, type_label); storage.store_category_config(child_path, type_label, expression); storage.add_child(_path, name); } catch (Storage.StorageError e) { throw new Core.EntityError.STORAGE_ERROR("Failed to create category: %s".printf(e.message)); } } // Create entity instance var category = new Category(_engine, child_path, type_label, expression); // Populate the index with existing documents try { category.populate_index(); } catch (Core.EngineError e) { warning("Failed to populate category index: %s", e.message); } // Register with hook manager for change notifications category.register_hooks(); // Notify engine of creation _engine.entity_created(category); return category; } /** * {@inheritDoc} * * Creates a new Catalogue child in this container. * * The catalogue will be populated with all existing documents grouped * by the key extracted from the expression, and will automatically * register for change notifications to keep its index up to date. * * @param name The name for the new catalogue * @param type_label The document type to catalogue * @param expression The expression to extract the grouping key * @return The created catalogue entity * @throws Core.EntityError.INVALID_PATH if name is empty * @throws Core.EntityError.ENTITY_ALREADY_EXISTS if an entity with this name already exists */ public override async Core.Entity? create_catalogue_async( string name, string type_label, string expression ) throws Core.EntityError { validate_can_create_child(name); var child_path = _path.append_child(name); // Check if we're in a transaction var embedded_engine = _engine as Engine.EmbeddedEngine; var tx = embedded_engine != null ? embedded_engine.current_transaction : null; if (tx != null) { // Queue operations in transaction tx.record_create_entity(child_path, Core.EntityType.CATALOGUE, type_label); tx.record_save_catalogue_config(child_path, type_label, expression); tx.record_add_child(_path, name); } else { // Direct write when not in transaction var storage = _engine.configuration.storage; try { storage.store_entity_metadata(child_path, Core.EntityType.CATALOGUE, type_label); storage.store_catalogue_config(child_path, type_label, expression); storage.add_child(_path, name); } catch (Storage.StorageError e) { throw new Core.EntityError.STORAGE_ERROR("Failed to create catalogue: %s".printf(e.message)); } } // Create entity instance var catalogue = new Catalogue(_engine, child_path, type_label, expression); // Populate the index with existing documents try { catalogue.populate_index(); } catch (Core.EngineError e) { warning("Failed to populate catalogue index: %s", e.message); } // Register with hook manager for change notifications catalogue.register_hooks(); // Notify engine of creation _engine.entity_created(catalogue); return catalogue; } /** * {@inheritDoc} * * Creates a new Index child in this container. * * The index will be populated with n-gram indices for all existing * documents of the configured type, and will automatically register * for change notifications to keep its index up to date. * * @param name The name for the new index * @param type_label The document type to index * @param expression The expression/property to index for text search * @return The created index entity * @throws Core.EntityError.INVALID_PATH if name is empty * @throws Core.EntityError.ENTITY_ALREADY_EXISTS if an entity with this name already exists */ public override async Core.Entity? create_index_async( string name, string type_label, string expression ) throws Core.EntityError { validate_can_create_child(name); var child_path = _path.append_child(name); // Check if we're in a transaction var embedded_engine = _engine as Engine.EmbeddedEngine; var tx = embedded_engine != null ? embedded_engine.current_transaction : null; if (tx != null) { // Queue operations in transaction tx.record_create_entity(child_path, Core.EntityType.INDEX, type_label); tx.record_save_category_config(child_path, type_label, expression); tx.record_add_child(_path, name); } else { // Direct write when not in transaction var storage = _engine.configuration.storage; try { storage.store_entity_metadata(child_path, Core.EntityType.INDEX, type_label); storage.store_category_config(child_path, type_label, expression); storage.add_child(_path, name); } catch (Storage.StorageError e) { throw new Core.EntityError.STORAGE_ERROR("Failed to create index: %s".printf(e.message)); } } // Create entity instance var index = new Index(_engine, child_path, type_label, expression); // Populate the n-gram index with existing documents try { index.populate_index(); } catch (Core.EngineError e) { warning("Failed to populate index: %s", e.message); } // Register with hook manager for change notifications index.register_hooks(); // Notify engine of creation _engine.entity_created(index); return index; } // === Child Deletion (Async) === /** * Deletes a child entity from this container. * * @param name The name of the child to delete * @throws Core.EntityError if deletion fails */ public async void delete_child_async(string name) throws Core.EntityError { var child = yield get_child_async(name); if (child == null) { throw new Core.EntityError.ENTITY_NOT_FOUND( "Child not found: %s".printf(name) ); } yield ((!) child).delete_async(); } // === Lifecycle (Async) === /** * {@inheritDoc} * * Deletes this container and all its children recursively. */ public override async void delete_async() throws Core.EntityError { // Delete all children first (recursively) var names = yield get_child_names_async(); foreach (var child_name in names) { var child = yield get_child_async(child_name); if (child != null) { try { yield ((!) child).delete_async(); } catch (Core.EntityError e) { warning("Failed to delete child %s: %s", child_name, e.message); } } } // Then delete this container yield base.delete_async(); } // === Validation === /** * Validates that a child with the given name can be created. * * @param name The name to validate * @throws Core.EntityError.INVALID_PATH if name is empty * @throws Core.EntityError.ENTITY_ALREADY_EXISTS if an entity already exists */ private void validate_can_create_child(string name) throws Core.EntityError { if (name == null || name == "") { throw new Core.EntityError.INVALID_PATH("Child name cannot be empty"); } var child_path = _path.append_child(name); var embedded = _engine as Engine.EmbeddedEngine; if (embedded != null && ((!) embedded).entity_exists_sync(child_path)) { throw new Core.EntityError.ENTITY_ALREADY_EXISTS( "Entity already exists: %s".printf(child_path.to_string()) ); } } } } // namespace Implexus.Entities