diff --git a/packages/database/lib/db.dart b/packages/database/lib/db.dart index 6caa3b7..ac784ce 100644 --- a/packages/database/lib/db.dart +++ b/packages/database/lib/db.dart @@ -1,3 +1,24 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/// This library provides core functionality for data management and persistence. +/// +/// It exports several modules: +/// - `managed`: Handles managed objects and their lifecycle. +/// - `persistent_store`: Provides interfaces for data persistence. +/// - `query`: Offers query building and execution capabilities. +/// - `schema`: Defines schema-related structures and operations. +/// +/// These modules collectively form a framework for efficient data handling, +/// storage, and retrieval within the Protevus Platform. +library; + export 'src/managed/managed.dart'; export 'src/persistent_store/persistent_store.dart'; export 'src/query/query.dart'; diff --git a/packages/database/lib/src/managed/attributes.dart b/packages/database/lib/src/managed/attributes.dart index 710f442..ba974da 100644 --- a/packages/database/lib/src/managed/attributes.dart +++ b/packages/database/lib/src/managed/attributes.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_database/src/managed/managed.dart'; import 'package:protevus_database/src/query/query.dart'; import 'package:meta/meta_meta.dart'; diff --git a/packages/database/lib/src/managed/backing.dart b/packages/database/lib/src/managed/backing.dart index 5080c64..4475bfd 100644 --- a/packages/database/lib/src/managed/backing.dart +++ b/packages/database/lib/src/managed/backing.dart @@ -1,6 +1,20 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_database/src/managed/managed.dart'; import 'package:protevus_database/src/managed/relationship_type.dart'; +/// An [ArgumentError] thrown when attempting to access an invalid property while building a `Query.values`. +/// +/// This error is thrown when attempting to access a property that is not backed by a column in the database table being inserted into. +/// This prohibits accessing `ManagedObject` and `ManagedSet` properties, except for `ManagedObject` properties with a `Relate` annotation. +/// For `Relate` properties, you may only set their primary key property. final ArgumentError _invalidValueConstruction = ArgumentError( "Invalid property access when building 'Query.values'. " "May only assign values to properties backed by a column of the table being inserted into. " @@ -8,6 +22,13 @@ final ArgumentError _invalidValueConstruction = ArgumentError( "properties with a 'Relate' annotation. For 'Relate' properties, you may only set their " "primary key property."); +/// A concrete implementation of [ManagedBacking] that stores the values of a [ManagedObject]. +/// +/// This class is responsible for managing the actual values of a [ManagedObject]. It provides methods to get and set the +/// values of the object's properties, and ensures that the values are valid according to the property's type. +/// +/// When setting a value for a property, this class checks if the value is assignable to the property's type. If the value +/// is not assignable, a [ValidationException] is thrown. class ManagedValueBacking extends ManagedBacking { @override Map contents = {}; @@ -31,6 +52,15 @@ class ManagedValueBacking extends ManagedBacking { } } +/// A concrete implementation of [ManagedBacking] that is designed to work with foreign key properties of a [ManagedObject]. +/// +/// This class is used when you need to create a new [ManagedObject] instance and only set its primary key property, which is +/// typically the foreign key property in a relationship. It allows you to set the primary key property without having to create +/// a full [ManagedObject] instance. +/// +/// The `ManagedForeignKeyBuilderBacking` class is useful when you are building a [Query] and need to set the foreign key property +/// of a related object, without creating the full related object. It ensures that only the primary key property can be set, and +/// throws an [ArgumentError] if you try to set any other properties. class ManagedForeignKeyBuilderBacking extends ManagedBacking { ManagedForeignKeyBuilderBacking(); ManagedForeignKeyBuilderBacking.from( @@ -65,6 +95,17 @@ class ManagedForeignKeyBuilderBacking extends ManagedBacking { } } +/// A concrete implementation of [ManagedBacking] that is designed to work with [ManagedObject] instances being used in a [Query.values]. +/// +/// This class is responsible for managing the values of a [ManagedObject] instance when it is being used to build a `Query.values` object. +/// It allows you to set the values of the object's properties, including its relationship properties, in a way that is compatible with the +/// constraints of the `Query.values` object. +/// +/// When setting a value for a property, this class checks the type of the property and ensures that the value being set is compatible with it. +/// For example, if the property is a [ManagedRelationshipDescription] with a `ManagedRelationshipType.belongsTo` relationship type, this class will +/// allow you to set the property to a [ManagedObject] instance or `null`, but not to a [ManagedSet] or other [ManagedObject] type. +/// +/// If you attempt to set an invalid value for a property, this class will throw an [ArgumentError] with a helpful error message. class ManagedBuilderBacking extends ManagedBacking { ManagedBuilderBacking(); ManagedBuilderBacking.from(ManagedEntity entity, ManagedBacking original) { @@ -92,9 +133,20 @@ class ManagedBuilderBacking extends ManagedBacking { }); } + /// The contents of the `ManagedValueBacking` class, which is a map that stores the values of a `ManagedObject`. @override Map contents = {}; + /// Retrieves the value for the given property in the `ManagedBacking` instance. + /// + /// If the property is a [ManagedRelationshipDescription] and not a `belongsTo` relationship, + /// an [ArgumentError] is thrown with the `_invalidValueConstruction` message. + /// + /// If the property is a [ManagedRelationshipDescription] and the key is not present in the + /// `contents` map, a new [ManagedObject] instance is created using the `ManagedForeignKeyBuilderBacking` + /// and stored in the `contents` map under the property name. + /// + /// The value for the property is then returned from the `contents` map. @override dynamic valueForProperty(ManagedPropertyDescription property) { if (property is ManagedRelationshipDescription) { @@ -111,6 +163,19 @@ class ManagedBuilderBacking extends ManagedBacking { return contents[property.name]; } + /// Sets the value for the specified property in the `ManagedBacking` instance. + /// + /// If the property is a [ManagedRelationshipDescription] and not a `belongsTo` relationship, + /// an [ArgumentError] is thrown with the `_invalidValueConstruction` message. + /// + /// If the property is a [ManagedRelationshipDescription] and the value is `null`, the + /// value in the `contents` map is set to `null`. + /// + /// If the property is a [ManagedRelationshipDescription] and the value is not `null`, + /// a new [ManagedObject] instance is created using the `ManagedForeignKeyBuilderBacking` + /// and stored in the `contents` map under the property name. + /// + /// For all other property types, the value is simply stored in the `contents` map. @override void setValueForProperty(ManagedPropertyDescription property, dynamic value) { if (property is ManagedRelationshipDescription) { @@ -136,6 +201,18 @@ class ManagedBuilderBacking extends ManagedBacking { } } +/// A concrete implementation of [ManagedBacking] that tracks the access of properties in a [ManagedObject]. +/// +/// This class is designed to monitor the access of properties in a [ManagedObject] instance. It keeps track of the +/// [KeyPath]s that are accessed, and when a property is accessed, it creates a new object or set based on the +/// type of the property. +/// +/// For [ManagedRelationshipDescription] properties, it creates a new instance of the destination entity with a +/// `ManagedAccessTrackingBacking` backing, or a [ManagedSet] for `hasMany` relationships. For [ManagedAttributeDescription] +/// properties with a document type, it creates a [DocumentAccessTracker] object. +/// +/// The `keyPaths` list keeps track of all the [KeyPath]s that have been accessed, and the `workingKeyPath` property +/// keeps track of the current [KeyPath] being built. class ManagedAccessTrackingBacking extends ManagedBacking { List keyPaths = []; KeyPath? workingKeyPath; @@ -179,6 +256,16 @@ class ManagedAccessTrackingBacking extends ManagedBacking { } } +/// A class that tracks access to a document property in a [ManagedObject]. +/// +/// This class is used in conjunction with the [ManagedAccessTrackingBacking] class to monitor +/// the access of document properties in a [ManagedObject] instance. When a document property +/// is accessed, a new instance of this class is created, and the [KeyPath] that represents +/// the access to the document property is updated. +/// +/// The `owner` property of this class holds the [KeyPath] that represents the access to the +/// document property. When the overridden `operator []` is called, it adds the key or index +/// used to access the document property to the `owner` [KeyPath]. class DocumentAccessTracker extends Document { DocumentAccessTracker(this.owner); diff --git a/packages/database/lib/src/managed/context.dart b/packages/database/lib/src/managed/context.dart index 9f3c9ea..3a9667d 100644 --- a/packages/database/lib/src/managed/context.dart +++ b/packages/database/lib/src/managed/context.dart @@ -1,5 +1,13 @@ -import 'dart:async'; +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import 'dart:async'; import 'package:protevus_openapi/documentable.dart'; import 'package:protevus_database/src/managed/data_model_manager.dart' as mm; import 'package:protevus_database/src/managed/managed.dart'; @@ -36,7 +44,7 @@ import 'package:protevus_database/src/query/query.dart'; /// } /// } class ManagedContext implements APIComponentDocumenter { - /// Creates an instance of [ManagedContext] from a [ManagedDataModel] and [PersistentStore]. + /// Creates a new instance of [ManagedContext] with the provided [dataModel] and [persistentStore]. /// /// This is the default constructor. /// @@ -47,15 +55,36 @@ class ManagedContext implements APIComponentDocumenter { _finalizer.attach(this, persistentStore, detach: this); } - /// Creates a child context from [parentContext]. + /// Creates a child [ManagedContext] from the provided [parentContext]. + /// + /// The created child context will share the same [persistentStore] and [dataModel] + /// as the [parentContext]. This allows you to perform database operations within + /// a transaction by creating a child context and executing queries on it. + /// + /// Example usage: + /// + /// await context.transaction((transaction) async { + /// final childContext = ManagedContext.childOf(transaction); + /// final query = Query(childContext)..values.name = 'John'; + /// await query.insert(); + /// }); ManagedContext.childOf(ManagedContext parentContext) : persistentStore = parentContext.persistentStore, dataModel = parentContext.dataModel; + /// A [Finalizer] that is used to automatically close the [PersistentStore] when the [ManagedContext] is destroyed. + /// + /// This [Finalizer] is attached to the [ManagedContext] instance in the constructor, and will call the `close()` method + /// of the [PersistentStore] when the [ManagedContext] is garbage collected or explicitly closed. This ensures that the + /// resources associated with the [PersistentStore] are properly cleaned up when the [ManagedContext] is no longer needed. static final Finalizer _finalizer = Finalizer((store) async => store.close()); /// The persistent store that [Query]s on this context are executed through. + /// + /// The [PersistentStore] is responsible for maintaining the connection to the database and + /// executing queries on behalf of the [ManagedContext]. This property holds the instance + /// of the persistent store that this [ManagedContext] will use to interact with the database. PersistentStore persistentStore; /// The data model containing the [ManagedEntity]s that describe the [ManagedObject]s this instance works with. @@ -78,9 +107,8 @@ class ManagedContext implements APIComponentDocumenter { /// [transactionBlock], roll back any changes made in the transaction, but this method will not /// throw. /// - /// TODO: the following statement is not true. /// Rollback takes a string but the transaction - /// returns . It would seem to be a better idea to still throw the manual Rollback + /// returns `Future`. It would seem to be a better idea to still throw the manual Rollback /// so the user has a consistent method of handling the rollback. We could add a property /// to the Rollback class 'manual' which would be used to indicate a manual rollback. /// For the moment I've changed the return type to Future as @@ -104,7 +132,7 @@ class ManagedContext implements APIComponentDocumenter { ); } - /// Closes this context and release its underlying resources. + /// Closes this [ManagedContext] and releases its underlying resources. /// /// This method closes the connection to [persistentStore] and releases [dataModel]. /// A context may not be reused once it has been closed. @@ -114,7 +142,7 @@ class ManagedContext implements APIComponentDocumenter { mm.remove(dataModel!); } - /// Returns an entity for a type from [dataModel]. + /// Returns the [ManagedEntity] for the given [type] from the [dataModel]. /// /// See [ManagedDataModel.entityForType]. ManagedEntity entityForType(Type type) { @@ -123,7 +151,20 @@ class ManagedContext implements APIComponentDocumenter { /// Inserts a single [object] into this context. /// - /// This method is equivalent shorthand for [Query.insert]. + /// This method is a shorthand for creating a [Query] with the provided [object] and + /// calling [Query.insert] to insert the object into the database. + /// + /// This method is useful when you need to insert a single object into the database. + /// If you need to insert multiple objects, consider using the [insertObjects] method + /// instead. + /// + /// Example usage: + /// + /// final user = User()..name = 'John Doe'; + /// await context.insertObject(user); + /// + /// @param object The [ManagedObject] instance to be inserted. + /// @return A [Future] that completes with the inserted [object] when the insert operation is complete. Future insertObject(T object) { final query = Query(this)..values = object; return query.insert(); @@ -131,8 +172,21 @@ class ManagedContext implements APIComponentDocumenter { /// Inserts each object in [objects] into this context. /// - /// If any insertion fails, no objects will be inserted into the database and an exception - /// is thrown. + /// This method takes a list of [ManagedObject] instances and inserts them into the + /// database in a single operation. If any of the insertions fail, no objects will + /// be inserted and an exception will be thrown. + /// + /// Example usage: + /// + /// final users = [ + /// User()..name = 'John Doe', + /// User()..name = 'Jane Doe', + /// ]; + /// await context.insertObjects(users); + /// + /// @param objects A list of [ManagedObject] instances to be inserted. + /// @return A [Future] that completes with a list of the inserted objects when the + /// insert operation is complete. Future> insertObjects( List objects, ) async { @@ -141,8 +195,23 @@ class ManagedContext implements APIComponentDocumenter { /// Returns an object of type [T] from this context if it exists, otherwise returns null. /// - /// If [T] cannot be inferred, an error is thrown. If [identifier] is not the same type as [T]'s primary key, - /// null is returned. + /// This method retrieves a single [ManagedObject] of type [T] from the database based on the provided [identifier]. + /// If the object of type [T] is found in the database, it is returned. If the object is not found, `null` is returned. + /// + /// If the type [T] cannot be inferred, an `ArgumentError` is thrown. Similarly, if the provided [identifier] is not + /// of the same type as the primary key of the [ManagedEntity] for type [T], `null` is returned. + /// + /// Example usage: + /// + /// final user = await context.fetchObjectWithID(1); + /// if (user != null) { + /// print('Found user: ${user.name}'); + /// } else { + /// print('User not found'); + /// } + /// + /// @param identifier The value of the primary key for the object of type [T] to fetch. + /// @return A [Future] that completes with the fetched object of type [T] if it exists, or `null` if it does not. Future fetchObjectWithID( dynamic identifier, ) async { @@ -162,12 +231,22 @@ class ManagedContext implements APIComponentDocumenter { return query.fetchOne(); } + /// Documents the components of the [ManagedContext] by delegating to the + /// [dataModel]'s [documentComponents] method. + /// + /// This method is part of the [APIComponentDocumenter] interface, which is + /// implemented by [ManagedContext]. It is responsible for generating + /// documentation for the components (such as [ManagedEntity] and + /// [ManagedAttribute]) that are part of the data model managed by this + /// [ManagedContext]. + /// + /// The documentation is generated and added to the provided [APIDocumentContext]. @override void documentComponents(APIDocumentContext context) => dataModel!.documentComponents(context); } -/// Throw this object to roll back a [ManagedContext.transaction]. +/// An exception that can be thrown to rollback a transaction in [ManagedContext.transaction]. /// /// When thrown in a transaction, it will cancel an in-progress transaction and rollback /// any changes it has made. diff --git a/packages/database/lib/src/managed/data_model.dart b/packages/database/lib/src/managed/data_model.dart index a8e02cf..0831667 100644 --- a/packages/database/lib/src/managed/data_model.dart +++ b/packages/database/lib/src/managed/data_model.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:collection/collection.dart' show IterableExtension; import 'package:protevus_openapi/documentable.dart'; import 'package:protevus_database/src/managed/managed.dart'; @@ -12,7 +21,6 @@ import 'package:protevus_runtime/runtime.dart'; /// all subclasses of [ManagedObject], building a [ManagedEntity] for each. /// /// Most applications do not need to access instances of this type. -/// class ManagedDataModel extends Object implements APIComponentDocumenter { /// Creates an instance of [ManagedDataModel] from a list of types that extend [ManagedObject]. It is preferable /// to use [ManagedDataModel.fromCurrentMirrorSystem] over this method. @@ -69,8 +77,23 @@ class ManagedDataModel extends Object implements APIComponentDocumenter { } } + /// Returns an [Iterable] of all [ManagedEntity] instances registered in this [ManagedDataModel]. + /// + /// This property provides access to the collection of all [ManagedEntity] instances that + /// were discovered and registered during the construction of this [ManagedDataModel]. Iterable get entities => _entities.values; + + /// Returns a [ManagedEntity] for a [Type]. + /// + /// [type] may be either a sub + /// [type] may be either a subclass of [ManagedObject] or a [ManagedObject]'s table definition. For example, the following + /// definition final Map _entities = {}; + + /// A map that associates table definitions to their corresponding [ManagedEntity] instances. + /// + /// This map is used to retrieve a [ManagedEntity] instance given a table definition type, + /// which can be useful when the type of the managed object is not known. final Map _tableDefinitionToEntityMap = {}; /// Returns a [ManagedEntity] for a [Type]. @@ -97,9 +120,24 @@ class ManagedDataModel extends Object implements APIComponentDocumenter { return entity; } + /// Attempts to retrieve a [ManagedEntity] for the given [Type]. + /// + /// This method first checks the [_entities] map for a direct match on the [Type]. If no match is found, + /// it then checks the [_tableDefinitionToEntityMap] for a match on the string representation of the [Type]. + /// + /// If a [ManagedEntity] is found, it is returned. Otherwise, `null` is returned. ManagedEntity? tryEntityForType(Type type) => _entities[type] ?? _tableDefinitionToEntityMap[type.toString()]; + /// Documents the components of the managed data model. + /// + /// This method iterates over all the [ManagedEntity] instances registered in this + /// [ManagedDataModel] and calls the `documentComponents` method on each one, passing + /// the provided [APIDocumentContext] instance. + /// + /// This allows each [ManagedEntity] to describe its own components, such as the + /// database table definition and the properties of the corresponding [ManagedObject] + /// subclass, in the context of the API documentation. @override void documentComponents(APIDocumentContext context) { for (final e in entities) { @@ -108,7 +146,11 @@ class ManagedDataModel extends Object implements APIComponentDocumenter { } } -/// Thrown when a [ManagedDataModel] encounters an error. +/// An error that is thrown when a [ManagedDataModel] encounters an issue. +/// +/// This error is used to indicate that there was a problem during the +/// construction or usage of a [ManagedDataModel] instance. The error +/// message provides information about the specific issue that occurred. class ManagedDataModelError extends Error { ManagedDataModelError(this.message); diff --git a/packages/database/lib/src/managed/data_model_manager.dart b/packages/database/lib/src/managed/data_model_manager.dart index 8c4b786..c77f498 100644 --- a/packages/database/lib/src/managed/data_model_manager.dart +++ b/packages/database/lib/src/managed/data_model_manager.dart @@ -1,8 +1,37 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_database/src/managed/data_model.dart'; import 'package:protevus_database/src/managed/entity.dart'; +/// A map that keeps track of the number of [ManagedDataModel] instances in the system. Map _dataModels = {}; +/// Finds a [ManagedEntity] for the specified [Type]. +/// +/// Searches through the [_dataModels] map to find the first [ManagedEntity] that +/// matches the given [Type]. If no matching [ManagedEntity] is found and [orElse] +/// is provided, the [orElse] function is called to provide a fallback +/// [ManagedEntity]. If no [ManagedEntity] is found and [orElse] is not provided, +/// a [StateError] is thrown. +/// +/// Parameters: +/// - `type`: The [Type] of the [ManagedEntity] to find. +/// - `orElse`: An optional function that returns a fallback [ManagedEntity] if +/// no match is found. +/// +/// Returns: +/// The found [ManagedEntity], or the result of calling [orElse] if provided and +/// no match is found. +/// +/// Throws: +/// A [StateError] if no [ManagedEntity] is found and [orElse] is not provided. ManagedEntity findEntity( Type type, { ManagedEntity Function()? orElse, @@ -23,10 +52,21 @@ ManagedEntity findEntity( return orElse(); } +/// Adds a [ManagedDataModel] to the [_dataModels] map, incrementing the count if it already exists +/// or setting the count to 1 if it's a new entry. +/// +/// Parameters: +/// - `dataModel`: The [ManagedDataModel] to be added to the map. void add(ManagedDataModel dataModel) { _dataModels.update(dataModel, (count) => count + 1, ifAbsent: () => 1); } +/// Removes a [ManagedDataModel] from the [_dataModels] map, decrementing the count if it already exists. +/// +/// If the count becomes less than 1, the [ManagedDataModel] is removed from the map completely. +/// +/// Parameters: +/// - `dataModel`: The [ManagedDataModel] to be removed from the map. void remove(ManagedDataModel dataModel) { if (_dataModels[dataModel] != null) { _dataModels.update(dataModel, (count) => count - 1); diff --git a/packages/database/lib/src/managed/document.dart b/packages/database/lib/src/managed/document.dart index 554d4af..bad4122 100644 --- a/packages/database/lib/src/managed/document.dart +++ b/packages/database/lib/src/managed/document.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_database/src/managed/managed.dart'; /// Allows storage of unstructured data in a [ManagedObject] property. diff --git a/packages/database/lib/src/managed/entity.dart b/packages/database/lib/src/managed/entity.dart index 9164b65..93dd0f2 100644 --- a/packages/database/lib/src/managed/entity.dart +++ b/packages/database/lib/src/managed/entity.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_openapi/documentable.dart'; import 'package:protevus_database/src/managed/backing.dart'; import 'package:protevus_database/src/managed/managed.dart'; @@ -133,6 +142,11 @@ class ManagedEntity implements APIComponentDocumenter { /// This is determined by the attribute with the [primaryKey] annotation. late String primaryKey; + /// Returns the primary key attribute of this entity. + /// + /// The primary key attribute is the [ManagedAttributeDescription] instance that represents the primary key + /// column of the database table associated with this entity. This property retrieves that attribute + /// by looking up the [primaryKey] property of this entity. ManagedAttributeDescription? get primaryKeyAttribute { return attributes[primaryKey]; } @@ -155,16 +169,39 @@ class ManagedEntity implements APIComponentDocumenter { return _tableName; } + /// The name of the table in the database that this entity maps to. + /// + /// By default, the table will be named by the table definition, e.g., a managed object declared as so will have a [tableName] of '_User'. + /// + /// class User extends ManagedObject<_User> implements _User {} + /// class _User { ... } + /// + /// You may implement the static method [tableName] on the table definition of a [ManagedObject] to return a [String] table + /// name override this default. final String _tableName; + + /// The list of default property names of this object. + /// + /// By default, a [Query] will fetch the properties in this list. You may specify + /// a different set of properties by setting the [Query.returningProperties] value. The default + /// set of properties is a list of all attributes that do not have the [Column.shouldOmitByDefault] flag + /// set in their [Column] and all [ManagedRelationshipType.belongsTo] relationships. + /// + /// This list cannot be modified. List? _defaultProperties; /// Derived from this' [tableName]. + /// + /// This overrides the default [hashCode] implementation for the [ManagedEntity] class. + /// The hash code is calculated based solely on the [tableName] property of the + /// [ManagedEntity] instance. This means that two [ManagedEntity] instances will be + /// considered equal (i.e., have the same hash code) if they have the same [tableName]. @override int get hashCode { return tableName.hashCode; } - /// Creates a new instance of this entity's instance type. + /// Creates a new instance of the [ManagedObject] subclass associated with this [ManagedEntity]. /// /// By default, the returned object will use a normal value backing map. /// If [backing] is non-null, it will be the backing map of the returned object. @@ -176,6 +213,14 @@ class ManagedEntity implements APIComponentDocumenter { return (runtime.instanceOfImplementation()..entity = this) as T; } + /// Creates a new [ManagedSet] of type [T] from the provided [objects]. + /// + /// The [objects] parameter should be an [Iterable] of dynamic values that can be + /// converted to instances of [T]. This method will use the [ManagedEntityRuntime] + /// implementation to create the appropriate [ManagedSet] instance. + /// + /// If the [objects] cannot be converted to instances of [T], this method will + /// return `null`. ManagedSet? setOf(Iterable objects) { return runtime.setOfImplementation(objects) as ManagedSet?; } @@ -297,6 +342,20 @@ class ManagedEntity implements APIComponentDocumenter { return tracker.keyPaths; } + /// Generates an API schema object for this managed entity. + /// + /// This method creates an [APISchemaObject] that represents the database table + /// associated with this managed entity. The schema object includes properties + /// for each attribute and relationship defined in the entity, excluding any + /// transient properties or properties that are not included in the default + /// result set. + /// + /// The schema object's title is set to the name of the entity, and the description + /// is set to a message indicating that no two objects may have the same value for + /// all of the unique properties defined for this entity (if any). + /// + /// The [APIDocumentContext] parameter is used to register the schema object + /// with the API document context. APISchemaObject document(APIDocumentContext context) { final schemaProperties = {}; final obj = APISchemaObject.object(schemaProperties)..title = name; @@ -326,11 +385,22 @@ class ManagedEntity implements APIComponentDocumenter { return obj; } - /// Two entities are considered equal if they have the same [tableName]. + /// Compares two [ManagedEntity] instances for equality based on their [tableName]. + /// + /// Two [ManagedEntity] instances are considered equal if they have the same [tableName]. @override bool operator ==(Object other) => other is ManagedEntity && tableName == other.tableName; + /// Provides a string representation of the [ManagedEntity] instance. + /// + /// The string representation includes the following information: + /// + /// - The name of the database table associated with the entity. + /// - A list of all attribute properties defined in the entity, with their string representations. + /// - A list of all relationship properties defined in the entity, with their string representations. + /// + /// This method is primarily intended for debugging and logging purposes. @override String toString() { final buf = StringBuffer(); @@ -349,6 +419,20 @@ class ManagedEntity implements APIComponentDocumenter { return buf.toString(); } + /// Generates an API schema object for this managed entity and registers it with the provided API document context. + /// + /// This method creates an [APISchemaObject] that represents the database table + /// associated with this managed entity. The schema object includes properties + /// for each attribute and relationship defined in the entity, excluding any + /// transient properties or properties that are not included in the default + /// result set. + /// + /// The schema object's title is set to the name of the entity, and the description + /// is set to a message indicating that no two objects may have the same value for + /// all of the unique properties defined for this entity (if any). + /// + /// The [APIDocumentContext] parameter is used to register the schema object + /// with the API document context. @override void documentComponents(APIDocumentContext context) { final obj = document(context); @@ -356,25 +440,139 @@ class ManagedEntity implements APIComponentDocumenter { } } +/// Defines the runtime implementation for a [ManagedEntity]. +/// +/// The `ManagedEntityRuntime` interface provides a set of methods that are used to implement the +/// runtime behavior of a [ManagedEntity]. This interface is used by the `ManagedEntity` class to +/// interact with the underlying runtime implementation, which may vary depending on the compilation +/// target (e.g., using mirrors or code generation). +/// +/// Implementers of this interface must provide the following functionality: +/// +/// - `finalize(ManagedDataModel dataModel)`: Perform any necessary finalization steps for the +/// managed entity, such as setting up caches or performing other initialization tasks. +/// - `instanceOfImplementation({ManagedBacking? backing})`: Create a new instance of the +/// [ManagedObject] associated with the managed entity, optionally using the provided backing +/// object. +/// - `setOfImplementation(Iterable objects)`: Create a new instance of [ManagedSet] for the +/// managed entity, using the provided objects. +/// - `setTransientValueForKey(ManagedObject object, String key, dynamic value)`: Set a transient +/// value for the specified key on the given [ManagedObject] instance. +/// - `getTransientValueForKey(ManagedObject object, String? key)`: Retrieve the transient value +/// for the specified key on the given [ManagedObject] instance. +/// - `isValueInstanceOf(dynamic value)`: Check if the provided value is an instance of the +/// [ManagedObject] associated with the managed entity. +/// - `isValueListOf(dynamic value)`: Check if the provided value is a list of instances of the +/// [ManagedObject] associated with the managed entity. +/// - `getPropertyName(Invocation invocation, ManagedEntity entity)`: Retrieve the property name +/// associated with the provided method invocation, given the managed entity. +/// - `dynamicConvertFromPrimitiveValue(ManagedPropertyDescription property, dynamic value)`: +/// Convert the provided primitive value to the appropriate type for the specified managed +/// property description. abstract class ManagedEntityRuntime { + /// Performs any necessary finalization steps for the managed entity, such as setting up caches or performing other initialization tasks. + /// + /// This method is called by the [ManagedDataModel] to finalize the managed entity after it has been created. Implementers of this interface + /// should use this method to perform any necessary setup or initialization tasks for the managed entity, such as building caches or + /// preparing other data structures. + /// + /// The [dataModel] parameter provides access to the overall [ManagedDataModel] that contains this managed entity, which may be useful for + /// performing finalization tasks that require information about the broader data model. void finalize(ManagedDataModel dataModel) {} + /// The entity associated with this managed object. + /// + /// This property provides access to the [ManagedEntity] instance that represents the database table + /// associated with this [ManagedObject]. The [ManagedEntity] contains metadata about the structure of + /// the database table, such as the names and types of its columns, and the relationships between + /// this table and other tables. ManagedEntity get entity; + /// Creates a new instance of this entity's instance type. + /// + /// By default, the returned object will use a normal value backing map. + /// If [backing] is non-null, it will be the backing map of the returned object. ManagedObject instanceOfImplementation({ManagedBacking? backing}); + /// Creates a new [ManagedSet] of the type associated with this managed entity from the provided [objects]. + /// + /// The [objects] parameter should be an [Iterable] of dynamic values that can be + /// converted to instances of the [ManagedObject] type associated with this managed entity. + /// This method will use the [ManagedEntityRuntime] implementation to create the appropriate + /// [ManagedSet] instance. + /// + /// If the [objects] cannot be converted to instances of the [ManagedObject] type, this + /// method will return `null`. ManagedSet setOfImplementation(Iterable objects); + /// Sets a transient value for the specified key on the given [ManagedObject] instance. + /// + /// The [object] parameter is the [ManagedObject] instance on which the transient value should be set. + /// The [key] parameter is the string identifier for the transient value that should be set. + /// The [value] parameter is the dynamic value that should be assigned to the transient property identified by the [key]. void setTransientValueForKey(ManagedObject object, String key, dynamic value); + /// Retrieves the transient value for the specified key on the given [ManagedObject] instance. + /// + /// The [object] parameter is the [ManagedObject] instance from which the transient value should be retrieved. + /// The [key] parameter is the string identifier for the transient value that should be retrieved. + /// This method returns the dynamic value associated with the transient property identified by the [key]. + /// If the [key] does not exist or is `null`, this method will return `null`. dynamic getTransientValueForKey(ManagedObject object, String? key); + /// Checks if the provided [value] is an instance of the [ManagedObject] associated with this [ManagedEntity]. + /// + /// This method is used to determine if a given value is an instance of the [ManagedObject] type that corresponds + /// to the current [ManagedEntity]. This is useful for validating the type of values that are being used with + /// this managed entity. + /// + /// The [value] parameter is the dynamic value to be checked. + /// + /// Returns `true` if the [value] is an instance of the [ManagedObject] associated with this [ManagedEntity], + /// and `false` otherwise. bool isValueInstanceOf(dynamic value); + /// Checks if the provided [value] is a list of instances of the [ManagedObject] associated with this [ManagedEntity]. + /// + /// This method is used to determine if a given value is a list of instances of the [ManagedObject] type that corresponds + /// to the current [ManagedEntity]. This is useful for validating the type of values that are being used with + /// this managed entity. + /// + /// The [value] parameter is the dynamic value to be checked. + /// + /// Returns `true` if the [value] is a list of instances of the [ManagedObject] associated with this [ManagedEntity], + /// and `false` otherwise. bool isValueListOf(dynamic value); + /// Retrieves the property name associated with the provided method invocation, given the managed entity. + /// + /// This method is used to determine the name of the property that a method invocation is accessing on a + /// [ManagedObject] instance. This information is often needed to properly handle the invocation and + /// interact with the managed entity. + /// + /// The [invocation] parameter is the [Invocation] object that represents the method invocation. + /// The [entity] parameter is the [ManagedEntity] instance that the method invocation is being performed on. + /// + /// Returns the property name associated with the provided method invocation, or `null` if the property + /// name cannot be determined. String? getPropertyName(Invocation invocation, ManagedEntity entity); + /// Converts the provided primitive [value] to the appropriate type for the specified [property]. + /// + /// This method is used to convert a dynamic value, such as one retrieved from a database or API, + /// into the correct type for a [ManagedPropertyDescription]. The [property] parameter specifies + /// the type of the property that the value should be converted to. + /// + /// The [value] parameter is the dynamic value to be converted. This method will attempt to + /// convert the [value] to the appropriate type for the [property], based on the property's + /// [ManagedPropertyType]. If the conversion is not possible, the method may return a value + /// that is not strictly type-compatible with the property, but is the closest possible + /// representation. + /// + /// The returned value will be of a type that is compatible with the [property]'s + /// [ManagedPropertyType]. If the conversion is not possible, the method may return a value + /// that is not strictly type-compatible with the property, but is the closest possible + /// representation. dynamic dynamicConvertFromPrimitiveValue( ManagedPropertyDescription property, dynamic value, diff --git a/packages/database/lib/src/managed/exception.dart b/packages/database/lib/src/managed/exception.dart index f251ce7..a1d3ce7 100644 --- a/packages/database/lib/src/managed/exception.dart +++ b/packages/database/lib/src/managed/exception.dart @@ -1,8 +1,19 @@ -import 'package:protevus_http/src/serializable.dart'; +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import 'package:protevus_http/http.dart'; /// An exception thrown when an ORM property validator is violated. /// -/// Behaves the same as [SerializableException]. +/// This exception behaves the same as [SerializableException]. It is used to +/// indicate that a validation error has occurred, such as when a property +/// value does not meet the expected criteria. class ValidationException extends SerializableException { ValidationException(super.errors); } diff --git a/packages/database/lib/src/managed/key_path.dart b/packages/database/lib/src/managed/key_path.dart index d7bb55d..b6a297d 100644 --- a/packages/database/lib/src/managed/key_path.dart +++ b/packages/database/lib/src/managed/key_path.dart @@ -1,25 +1,149 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_database/src/managed/managed.dart'; +/// A class that represents a path to a property in a managed object. +/// +/// The `KeyPath` class is used to represent a path to a property within a managed object. +/// It provides methods to create new `KeyPath` instances by removing or adding keys to an +/// existing `KeyPath`. +/// +/// The `path` field is a list of `ManagedPropertyDescription` objects, which represent +/// the individual properties that make up the path. The `dynamicElements` field is used +/// to store any dynamic elements that are part of the path. +/// +/// Example usage: +/// ```dart +/// final keyPath = KeyPath(managedObject.property); +/// final newKeyPath = KeyPath.byAddingKey(keyPath, managedObject.anotherProperty); +/// ``` class KeyPath { + /// Constructs a new `KeyPath` instance with the given root property. + /// + /// The `path` field of the `KeyPath` instance will be initialized with a single + /// `ManagedPropertyDescription` object, which represents the root property. + /// + /// This constructor is typically used as the starting point for building a `KeyPath` + /// instance, which can then be further modified using the other constructors and + /// methods provided by the `KeyPath` class. + /// + /// Example: + /// ```dart + /// final keyPath = KeyPath(managedObject.property); + /// ``` KeyPath(ManagedPropertyDescription? root) : path = [root]; + /// Creates a new `KeyPath` instance by removing the first `offset` keys from the original `KeyPath`. + /// + /// This constructor is useful when you want to create a new `KeyPath` that represents a sub-path of an existing `KeyPath`. + /// + /// The `original` parameter is the `KeyPath` instance from which the new `KeyPath` will be derived. + /// The `offset` parameter specifies the number of keys to remove from the beginning of the `original` `KeyPath`. + /// + /// The resulting `KeyPath` instance will have a `path` list that contains the remaining keys, starting from the `offset`-th key. + /// + /// Example: + /// ```dart + /// final originalKeyPath = KeyPath(managedObject.property1).byAddingKey(managedObject.property2); + /// final subKeyPath = KeyPath.byRemovingFirstNKeys(originalKeyPath, 1); + /// // The `subKeyPath` will have a `path` list containing only `managedObject.property2` + /// ``` KeyPath.byRemovingFirstNKeys(KeyPath original, int offset) : path = original.path.sublist(offset); + /// Constructs a new `KeyPath` instance by adding a new key to the end of an existing `KeyPath`. + /// + /// This constructor is useful when you want to create a new `KeyPath` that represents a longer path + /// by adding a new property to the end of an existing `KeyPath`. + /// + /// The `original` parameter is the `KeyPath` instance to which the new key will be added. + /// The `key` parameter is the `ManagedPropertyDescription` of the new property to be added to the `KeyPath`. + /// + /// The resulting `KeyPath` instance will have a `path` list that contains all the keys from the `original` + /// `KeyPath`, plus the new `key` added to the end. + /// + /// Example: + /// ```dart + /// final originalKeyPath = KeyPath(managedObject.property1); + /// final newKeyPath = KeyPath.byAddingKey(originalKeyPath, managedObject.property2); + /// // The `newKeyPath` will have a `path` list containing both `managedObject.property1` and `managedObject.property2` + /// ``` KeyPath.byAddingKey(KeyPath original, ManagedPropertyDescription key) : path = List.from(original.path)..add(key); + /// A list of `ManagedPropertyDescription` objects that represent the individual properties + /// that make up the path of the `KeyPath` instance. The order of the properties in the + /// list corresponds to the order of the path. + /// + /// This field is used to store the individual properties that make up the path of the `KeyPath`. + /// Each `ManagedPropertyDescription` object in the list represents a single property in the path. + /// The order of the properties in the list corresponds to the order of the path, with the first + /// property in the path being the first element in the list, and so on. final List path; + + /// A list of dynamic elements that are part of the key path. + /// + /// The `dynamicElements` field is used to store any dynamic elements that are part of the `KeyPath`. This allows the `KeyPath` to represent paths that include dynamic or variable elements, in addition to the static property descriptions stored in the `path` field. List? dynamicElements; + /// Returns the `ManagedPropertyDescription` at the specified `index` in the `path` list. + /// + /// This operator allows you to access the individual `ManagedPropertyDescription` objects that make up the `KeyPath` instance, using an index. + /// + /// Example: + /// ```dart + /// final keyPath = KeyPath(managedObject.property1).byAddingKey(managedObject.property2); + /// final secondProperty = keyPath[1]; // Returns the `ManagedPropertyDescription` for `managedObject.property2` + /// ``` ManagedPropertyDescription? operator [](int index) => path[index]; + /// Returns the number of properties in the key path. + /// + /// This getter returns the length of the `path` list, which represents the number of + /// properties that make up the key path. This can be useful when you need to know + /// how many properties are in the key path, for example, when iterating over them + /// or performing other operations that require the length of the key path. int get length => path.length; + /// Adds a new `ManagedPropertyDescription` to the end of the `path` list. + /// + /// This method is used to add a new property description to the `KeyPath` instance. + /// The new property description will be appended to the end of the `path` list, effectively + /// extending the key path. + /// + /// This can be useful when you need to create a new `KeyPath` by adding additional properties + /// to an existing `KeyPath` instance. + /// + /// Example: + /// ```dart + /// final keyPath = KeyPath(managedObject.property1); + /// keyPath.add(managedObject.property2); + /// // The `keyPath` now represents the path "property1.property2" + /// ``` void add(ManagedPropertyDescription element) { path.add(element); } + /// Adds a dynamic element to the `dynamicElements` list. + /// + /// This method is used to add a new dynamic element to the `dynamicElements` list of the `KeyPath` instance. + /// The `dynamicElements` list is used to store any dynamic or variable elements that are part of the key path, in + /// addition to the static property descriptions stored in the `path` list. + /// + /// If the `dynamicElements` list is `null`, it will be initialized before adding the new element. + /// + /// Example: + /// ```dart + /// final keyPath = KeyPath(managedObject.property1); + /// keyPath.addDynamicElement(someVariable); + /// ``` void addDynamicElement(dynamic element) { dynamicElements ??= []; dynamicElements!.add(element); diff --git a/packages/database/lib/src/managed/managed.dart b/packages/database/lib/src/managed/managed.dart index 34a4026..f472767 100644 --- a/packages/database/lib/src/managed/managed.dart +++ b/packages/database/lib/src/managed/managed.dart @@ -1,3 +1,35 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/// This library file serves as a central export point for various components +/// of the Protevus Platform's data model and validation system. +/// +/// It exports the following modules: +/// - attributes: Likely contains attribute-related functionality +/// - context: Probably defines context-related classes or functions +/// - data_model: Likely contains core data model structures +/// - document: Possibly related to document handling or representation +/// - entity: Likely defines entity-related classes or functions +/// - exception: Probably contains custom exception classes +/// - object: Likely contains object-related utilities or base classes +/// - property_description: Possibly related to describing object properties +/// - set: Likely contains set-related functionality +/// - type: Probably includes type-related utilities or definitions +/// - validation/managed: Likely contains managed validation functionality +/// - validation/metadata: Probably includes metadata-based validation +/// - key_path: Likely related to handling key paths in data structures +/// +/// This library file allows users to import all these components with a single +/// import statement, simplifying the use of the Protevus Platform's core +/// functionalities in other parts of the application. +library; + export 'attributes.dart'; export 'context.dart'; export 'data_model.dart'; diff --git a/packages/database/lib/src/managed/object.dart b/packages/database/lib/src/managed/object.dart index c04b82a..5752e9e 100644 --- a/packages/database/lib/src/managed/object.dart +++ b/packages/database/lib/src/managed/object.dart @@ -1,13 +1,22 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_openapi/documentable.dart'; import 'package:protevus_database/src/managed/backing.dart'; import 'package:protevus_database/src/managed/data_model_manager.dart' as mm; import 'package:protevus_database/src/managed/managed.dart'; import 'package:protevus_database/src/query/query.dart'; -import 'package:protevus_http/src/serializable.dart'; +import 'package:protevus_http/http.dart'; import 'package:protevus_openapi/v3.dart'; import 'package:meta/meta.dart'; -/// Instances of this class provide storage for [ManagedObject]s. +/// An abstract class that provides storage for [ManagedObject] instances. /// /// This class is primarily used internally. /// @@ -23,13 +32,25 @@ import 'package:meta/meta.dart'; /// Conduit implements concrete subclasses of this class to provide behavior for property storage /// and query-building. abstract class ManagedBacking { - /// Retrieve a property by its entity and name. + /// Retrieves the value of the specified [ManagedPropertyDescription] property. + /// + /// This method is used to get the value of a property from the [ManagedBacking] instance. + /// + /// Parameters: + /// - [property]: The [ManagedPropertyDescription] for the property to retrieve. + /// + /// Returns: + /// The value of the specified property. dynamic valueForProperty(ManagedPropertyDescription property); - /// Sets a property by its entity and name. + /// Sets the value of the specified [ManagedPropertyDescription] property to the provided [value]. + /// + /// Parameters: + /// - [property]: The [ManagedPropertyDescription] of the property to be set. + /// - [value]: The value to be set for the specified property. void setValueForProperty(ManagedPropertyDescription property, dynamic value); - /// Removes a property from this instance. + /// Removes a property from the backing map of this [ManagedBacking] instance. /// /// Use this method to use any reference of a property from this instance. void removeProperty(String propertyName) { @@ -37,10 +58,14 @@ abstract class ManagedBacking { } /// A map of all set values of this instance. + /// + /// This property returns a map that contains all the properties that have been set + /// on this instance of `ManagedBacking`. The keys in the map are the property names, + /// and the values are the corresponding property values. Map get contents; } -/// An object that represents a database row. +/// An abstract class that provides storage for [ManagedObject] instances. /// /// This class must be subclassed. A subclass is declared for each table in a database. These subclasses /// create the data model of an application. @@ -66,37 +91,82 @@ abstract class ManagedBacking { /// See more documentation on defining a data model at http://conduit.io/docs/db/modeling_data/ abstract class ManagedObject extends Serializable { /// IMPROVEMENT: Cache of entity.properties to reduce property loading time + /// + /// This code caches the entity's properties in a `Map` to + /// improve the performance of accessing these properties. By caching the properties, the code + /// avoids having to load them from the `entity` object every time they are needed, which can + /// improve the overall performance of the application. late Map properties = entity.properties; - /// Cache of entity.properties using ResponseKey name as key, in case no ResponseKey is set then default property name is used as key + /// A cache of the `entity.properties` map, using the response key name as the key. + /// + /// If a property does not have a response key set, the default property name is used as the key instead. + /// This cache is used to improve the performance of accessing the property information, as it avoids having to + /// look up the properties in the `entity.properties` map every time they are needed. late Map responseKeyProperties = { for (final key in properties.keys) mapKeyName(key): properties[key] }; + /// A flag that determines whether to include a property with a null value in the output map. + /// + /// When the `ManagedObject` has no properties or the first property's response model has `includeIfNullField` set to `true`, + /// this flag is set to `true`, indicating that null values should be included in the output map. + /// Otherwise, it is set to `false`, and null values will be omitted from the output map. late final bool modelFieldIncludeIfNull = properties.isEmpty || (properties.values.first?.responseModel?.includeIfNullField ?? true); + /// Determines the key name to use for a property when serializing the model to a map. + /// + /// This method first checks if the property has a response key set, and if so, uses that as the key name. + /// If the property does not have a response key, it uses the property name. + /// If the property name is null, it falls back to using the original property name. + /// + /// This allows the model to control the key names used in the serialized output, which can be useful for + /// maintaining consistent naming conventions or working with external APIs that have specific key naming requirements. + /// + /// Parameters: + /// - `propertyName`: The name of the property to get the key name for. + /// + /// Returns: + /// The key name to use for the property when serializing the model to a map. String mapKeyName(String propertyName) { final property = properties[propertyName]; return property?.responseKey?.name ?? property?.name ?? propertyName; } + /// A flag that determines whether this class should be automatically documented. + /// + /// If `true`, the class will be automatically documented, typically as part of an API documentation generation process. + /// If `false`, the class will not be automatically documented, and any documentation for it must be added manually. static bool get shouldAutomaticallyDocument => false; /// The [ManagedEntity] this instance is described by. + /// + /// This property holds the [ManagedEntity] that describes the table definition for the managed object + /// of type `T`. The [ManagedEntity] is used to provide metadata about the object, such as its + /// properties, relationships, and validation rules. ManagedEntity entity = mm.findEntity(T); /// The persistent values of this object. /// - /// Values stored by this object are stored in [backing]. A backing is a [Map], where each key - /// is a property name of this object. A backing adds some access logic to storing and retrieving - /// its key-value pairs. + /// This property represents the persistent values of the current `ManagedObject` instance. The values are stored in a + /// [ManagedBacking] object, which is a `Map` where the keys are property names and the values are the corresponding + /// property values. /// /// You rarely need to use [backing] directly. There are many implementations of [ManagedBacking] /// for fulfilling the behavior of the ORM, so you cannot rely on its behavior. ManagedBacking backing = ManagedValueBacking(); /// Retrieves a value by property name from [backing]. + /// + /// This operator overload allows you to access the value of a property on the `ManagedObject` instance + /// using the bracket notation (`instance[propertyName]`). + /// + /// Parameters: + /// - `propertyName`: The name of the property to retrieve the value for. + /// + /// Returns: + /// The value of the specified property, or throws an `ArgumentError` if the property does not exist on the entity. dynamic operator [](String propertyName) { final prop = properties[propertyName]; if (prop == null) { @@ -108,6 +178,16 @@ abstract class ManagedObject extends Serializable { } /// Sets a value by property name in [backing]. + /// + /// This operator overload allows you to set the value of a property on the `ManagedObject` instance + /// using the bracket notation (`instance[propertyName] = value`). + /// + /// Parameters: + /// - `propertyName`: The name of the property to set the value for. + /// - `value`: The value to set for the specified property. + /// + /// Throws: + /// - `ArgumentError` if the specified `propertyName` does not exist on the entity. void operator []=(String? propertyName, dynamic value) { final prop = properties[propertyName]; if (prop == null) { @@ -120,12 +200,20 @@ abstract class ManagedObject extends Serializable { /// Removes a property from [backing]. /// - /// This will remove a value from the backing map. + /// This method removes the specified property from the backing map of the `ManagedBacking` instance. + /// + /// Parameters: + /// - `propertyName`: The name of the property to remove from the backing map. void removePropertyFromBackingMap(String propertyName) { backing.removeProperty(propertyName); } /// Removes multiple properties from [backing]. + /// + /// This method removes the specified properties from the backing map of the `ManagedBacking` instance. + /// + /// Parameters: + /// - `propertyNames`: A list of property names to remove from the backing map. void removePropertiesFromBackingMap(List propertyNames) { for (final propertyName in propertyNames) { backing.removeProperty(propertyName); @@ -133,6 +221,15 @@ abstract class ManagedObject extends Serializable { } /// Checks whether or not a property has been set in this instances' [backing]. + /// + /// This method checks if the specified property name exists as a key in the [contents] map of the [backing] object. + /// It returns `true` if the property has been set, and `false` otherwise. + /// + /// Parameters: + /// - `propertyName`: The name of the property to check for. + /// + /// Returns: + /// `true` if the property has been set in the [backing] object, `false` otherwise. bool hasValueForProperty(String propertyName) { return backing.contents.containsKey(propertyName); } @@ -164,8 +261,7 @@ abstract class ManagedObject extends Serializable { /// This method is invoked prior to validation and therefore any values modified in this method /// are subject to the validation behavior of this instance. /// - /// An example implementation would set the 'createdDate' of an object when it is first created - /// + /// An example implementation would set the 'createdDate' of an object when it is first created: /// @override /// void willInsert() { /// createdDate = new DateTime.now().toUtc(); @@ -200,6 +296,18 @@ abstract class ManagedObject extends Serializable { return ManagedValidator.run(this, event: forEvent); } + /// Provides dynamic handling of property access and updates. + /// + /// This `noSuchMethod` implementation allows for dynamic access and updates to properties of the `ManagedObject`. + /// + /// When an unknown method is called on the `ManagedObject`, this implementation will check if the method name + /// corresponds to a property on the entity. If it does, it will return the value of the property if the method + /// is a getter, or set the value of the property if the method is a setter. + /// + /// If the method name does not correspond to a property, the default `NoSuchMethodError` is thrown. + /// + /// This implementation provides a more convenient way to access and update properties compared to using the + /// square bracket notation (`[]` and `[]=`). @override dynamic noSuchMethod(Invocation invocation) { final propertyName = entity.runtime.getPropertyName(invocation, entity); @@ -216,6 +324,31 @@ abstract class ManagedObject extends Serializable { throw NoSuchMethodError.withInvocation(this, invocation); } + /// Reads the values from the provided [object] map and sets them on the [ManagedObject] instance. + /// + /// This method iterates over the key-value pairs in the [object] map and sets the corresponding + /// properties on the [ManagedObject] instance. It checks the following: + /// + /// - If the key in the [object] map does not correspond to a property in the [responseKeyProperties] + /// map, it throws a [ValidationException] with the error message "invalid input key 'key'". + /// - If the property is marked as private (its name starts with an underscore), it throws a + /// [ValidationException] with the error message "invalid input key 'key'". + /// - If the property is a [ManagedAttributeDescription]: + /// - If the property is not transient, it sets the value on the [backing] object using the + /// [convertFromPrimitiveValue] method of the property. + /// - If the property is transient, it checks if the property is available as input. If not, it + /// throws a [ValidationException] with the error message "invalid input key 'key'". Otherwise, + /// it sets the transient value on the [ManagedObject] instance using the + /// [setTransientValueForKey] method of the [entity.runtime]. + /// - For all other properties, it sets the value on the [backing] object using the + /// [convertFromPrimitiveValue] method of the property. + /// + /// Parameters: + /// - [object]: A map of the values to be set on the [ManagedObject] instance. + /// + /// Throws: + /// - [ValidationException] if any of the input keys are invalid or the values cannot be converted + /// to the appropriate type. @override void readFromMap(Map object) { object.forEach((key, v) { @@ -292,13 +425,45 @@ abstract class ManagedObject extends Serializable { return outputMap; } + /// Generates an [APISchemaObject] that describes the schema of the managed object. + /// + /// This method is used to generate an [APISchemaObject] that describes the schema of the managed object. The resulting + /// schema object can be used in OpenAPI/Swagger documentation or other API documentation tools. + /// + /// The [APIDocumentContext] parameter is used to provide contextual information about the API documentation being generated. + /// This context is passed to the [ManagedEntity.document] method, which is responsible for generating the schema object. + /// + /// Returns: + /// The [APISchemaObject] that describes the schema of the managed object. @override APISchemaObject documentSchema(APIDocumentContext context) => entity.document(context); + /// Checks if a property is private. + /// + /// This method checks whether the given property name starts with an underscore, + /// which is a common convention in Dart to indicate a private property. + /// + /// Parameters: + /// - `propertyName`: The name of the property to check. + /// + /// Returns: + /// `true` if the property name starts with an underscore, indicating that the + /// property is private, and `false` otherwise. static bool _isPropertyPrivate(String propertyName) => propertyName.startsWith("_"); + /// Determines whether to include a property with a null value in the output map. + /// + /// This method checks the `includeIfNull` property of the `responseKey` associated with the + /// given `ManagedPropertyDescription`. If the `responseKey` has an `includeIfNull` value set, + /// that value is used. Otherwise, the `modelFieldIncludeIfNull` flag is used. + /// + /// Parameters: + /// - `property`: The `ManagedPropertyDescription` to check for the `includeIfNull` setting. + /// + /// Returns: + /// `true` if a property with a null value should be included in the output map, `false` otherwise. bool _includeIfNull(ManagedPropertyDescription property) => property.responseKey?.includeIfNull ?? modelFieldIncludeIfNull; } diff --git a/packages/database/lib/src/managed/property_description.dart b/packages/database/lib/src/managed/property_description.dart index 0c12665..fa9b808 100644 --- a/packages/database/lib/src/managed/property_description.dart +++ b/packages/database/lib/src/managed/property_description.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_openapi/documentable.dart'; import 'package:protevus_database/src/managed/managed.dart'; import 'package:protevus_database/src/managed/relationship_type.dart'; @@ -12,6 +21,25 @@ import 'package:protevus_runtime/runtime.dart'; /// about the property such as its name and type. Those properties are represented by concrete subclasses of this class, [ManagedRelationshipDescription] /// and [ManagedAttributeDescription]. abstract class ManagedPropertyDescription { + /// Initializes a new instance of [ManagedPropertyDescription]. + /// + /// The [ManagedPropertyDescription] class represents a property of a [ManagedObject] object. This constructor sets the basic properties of the + /// [ManagedPropertyDescription] instance, such as the entity, name, type, declared type, uniqueness, indexing, nullability, inclusion in default result sets, + /// autoincrement, validators, response model, and response key. + /// + /// Parameters: + /// - [entity]: The [ManagedEntity] that contains this property. + /// - [name]: The identifying name of this property. + /// - [type]: The value type of this property, indicating the Dart type and database column type. + /// - [declaredType]: The type of the variable that this property represents. + /// - [unique]: Whether or not this property must be unique across all instances represented by [entity]. Defaults to `false`. + /// - [indexed]: Whether or not this property should be indexed by a [PersistentStore]. Defaults to `false`. + /// - [nullable]: Whether or not this property can be null. Defaults to `false`. + /// - [includedInDefaultResultSet]: Whether or not this property is returned in the default set of [Query.returningProperties]. Defaults to `true`. + /// - [autoincrement]: Whether or not this property should use an auto-incrementing scheme. Defaults to `false`. + /// - [validators]: A list of [ManagedValidator]s for this instance. + /// - [responseModel]: The [ResponseModel] associated with this property. + /// - [responseKey]: The [ResponseKey] associated with this property. ManagedPropertyDescription( this.entity, this.name, @@ -36,44 +64,64 @@ abstract class ManagedPropertyDescription { } /// A reference to the [ManagedEntity] that contains this property. + /// + /// The [ManagedEntity] that this [ManagedPropertyDescription] belongs to. This property provides a way to access the entity that + /// manages the data represented by this property. final ManagedEntity entity; /// The value type of this property. /// - /// Will indicate the Dart type and database column type of this property. + /// This property indicates the Dart type and database column type of this property. It is used to determine how the property + /// should be stored and retrieved from the database, as well as how it should be represented in the application's data model. final ManagedType? type; /// The identifying name of this property. + /// + /// This field represents the name of the property being described by this [ManagedPropertyDescription] instance. + /// The name is used to uniquely identify the property within the [ManagedEntity] that it belongs to. final String name; /// Whether or not this property must be unique to across all instances represented by [entity]. /// - /// Defaults to false. + /// This property determines if the value of this property must be unique across all instances of the [ManagedObject] that this [ManagedPropertyDescription] belongs to. If set to `true`, the [PersistentStore] will ensure that no two instances have the same value for this property. + /// + /// Defaults to `false`. final bool isUnique; /// Whether or not this property should be indexed by a [PersistentStore]. /// - /// Defaults to false. + /// When set to `true`, the [PersistentStore] will create an index for this property, which can improve the performance of + /// queries that filter or sort on this property. This is useful for properties that are frequently used in queries, but it + /// may come at the cost of increased storage requirements and write latency. + /// + /// Defaults to `false`. final bool isIndexed; /// Whether or not this property can be null. /// - /// Defaults to false. + /// This property determines if the value of this property can be `null` or not. If set to `true`, the [ManagedObject] that this + /// [ManagedPropertyDescription] belongs to can have a `null` value for this property. If set to `false`, the [ManagedObject] + /// cannot have a `null` value for this property. + /// + /// Defaults to `false`. final bool isNullable; /// Whether or not this property is returned in the default set of [Query.returningProperties]. /// - /// This defaults to true. If true, when executing a [Query] that does not explicitly specify [Query.returningProperties], - /// this property will be returned. If false, you must explicitly specify this property in [Query.returningProperties] to retrieve it from persistent storage. + /// This defaults to `true`. If `true`, when executing a [Query] that does not explicitly specify [Query.returningProperties], + /// this property will be returned. If `false`, you must explicitly specify this property in [Query.returningProperties] to retrieve it from persistent storage. final bool isIncludedInDefaultResultSet; /// Whether or not this property should use an auto-incrementing scheme. /// - /// By default, false. When true, it signals to the [PersistentStore] that this property should automatically be assigned a value - /// by the database. + /// When this is set to `true`, it signals to the [PersistentStore] that this property should automatically be assigned a value + /// by the database. This is commonly used for primary key properties that should have a unique, incrementing value for each new + /// instance of the [ManagedObject]. + /// + /// Defaults to `false`. final bool autoincrement; - /// Whether or not this attribute is private or not. + /// Determines whether the current property is marked as private. /// /// Private variables are prefixed with `_` (underscores). This properties are not read /// or written to maps and cannot be accessed from outside the class. @@ -83,15 +131,44 @@ abstract class ManagedPropertyDescription { return name.startsWith("_"); } - /// [ManagedValidator]s for this instance. + /// The list of [ManagedValidator]s associated with this instance. + /// + /// [ManagedValidator]s are used to validate the values of this property + /// before they are stored in the database. The `validators` property + /// returns a read-only list of these validators. List get validators => _validators; + /// The list of [ManagedValidator]s associated with this instance. + /// + /// [ManagedValidator]s are used to validate the values of this property + /// before they are stored in the database. The `validators` property + /// returns a read-only list of these validators. final List _validators; + /// The [ResponseModel] associated with this property. + /// + /// The [ResponseModel] defines the structure of the response + /// that will be returned for this property. This allows for + /// customization of the documentation and schema for this + /// property, beyond the default behavior. final ResponseModel? responseModel; + + /// The [ResponseKey] associated with this property. + /// + /// The [ResponseKey] defines the key that will be used for this + /// property in the response object. This allows for customization + /// of the property names in the response, beyond the default + /// behavior. final ResponseKey? responseKey; - /// Whether or not a the argument can be assigned to this property. + /// Determines whether the provided Dart value can be assigned to this property. + /// + /// This method checks if the given `dartValue` is compatible with the type of this property. + /// It delegates the type checking to the `isAssignableWith` method of the [ManagedType] associated with this property. + /// + /// Returns: + /// - `true` if the `dartValue` can be assigned to this property. + /// - `false` otherwise. bool isAssignableWith(dynamic dartValue) => type!.isAssignableWith(dartValue); /// Converts a value from a more complex value into a primitive value according to this instance's definition. @@ -99,23 +176,49 @@ abstract class ManagedPropertyDescription { /// This method takes a Dart representation of a value and converts it to something that can /// be used elsewhere (e.g. an HTTP body or database query). How this value is computed /// depends on this instance's definition. + /// + /// Parameters: + /// - [value]: The Dart representation of the value to be converted. + /// + /// Returns: + /// The converted primitive value. dynamic convertToPrimitiveValue(dynamic value); /// Converts a value to a more complex value from a primitive value according to this instance's definition. /// /// This method takes a non-Dart representation of a value (e.g. an HTTP body or database query) - /// and turns it into a Dart representation . How this value is computed + /// and turns it into a Dart representation. How this value is computed /// depends on this instance's definition. dynamic convertFromPrimitiveValue(dynamic value); /// The type of the variable that this property represents. + /// + /// This property represents the Dart type of the variable that the [ManagedPropertyDescription] instance + /// is describing. It is used to ensure that the value assigned to this property is compatible with the + /// expected type. final Type? declaredType; /// Returns an [APISchemaObject] that represents this property. /// - /// Used during documentation. + /// This method generates an [APISchemaObject] that describes the schema of this property, which can be used for API documentation. + /// + /// Parameters: + /// - [context]: The [APIDocumentContext] that provides information about the current documentation context. + /// + /// Returns: + /// An [APISchemaObject] that represents the schema of this property. APISchemaObject documentSchemaObject(APIDocumentContext context); + /// Creates a typed API schema object based on the provided [ManagedType]. + /// + /// This method generates an [APISchemaObject] that represents the schema of a property based on its + /// [ManagedType]. The generated schema object can be used for API documentation and other purposes. + /// + /// Parameters: + /// - [type]: The [ManagedType] that the schema object should be generated for. + /// + /// Returns: + /// An [APISchemaObject] that represents the schema of the provided [ManagedType]. static APISchemaObject _typedSchemaObject(ManagedType type) { switch (type.kind) { case ManagedPropertyType.integer: @@ -157,6 +260,26 @@ abstract class ManagedPropertyDescription { /// Each scalar property [ManagedObject] object persists is described by an instance of [ManagedAttributeDescription]. This class /// adds two properties to [ManagedPropertyDescription] that are only valid for non-relationship types, [isPrimaryKey] and [defaultValue]. class ManagedAttributeDescription extends ManagedPropertyDescription { + /// This constructor is used to create a [ManagedAttributeDescription] instance, which represents a scalar property of a [ManagedObject]. + /// It initializes the properties of the [ManagedPropertyDescription] base class, and also sets the `isPrimaryKey` and `defaultValue` properties + /// specific to [ManagedAttributeDescription]. + /// + /// Parameters: + /// - `entity`: The [ManagedEntity] that contains this property. + /// - `name`: The identifying name of this property. + /// - `type`: The value type of this property, indicating the Dart type and database column type. + /// - `declaredType`: The type of the variable that this property represents. + /// - `transientStatus`: The validity of this attribute as input, output or both. + /// - `primaryKey`: Whether or not this attribute is the primary key for its [ManagedEntity]. Defaults to `false`. + /// - `defaultValue`: The default value for this attribute. Defaults to `null`. + /// - `unique`: Whether or not this property must be unique across all instances represented by `entity`. Defaults to `false`. + /// - `indexed`: Whether or not this property should be indexed by a [PersistentStore]. Defaults to `false`. + /// - `nullable`: Whether or not this property can be null. Defaults to `false`. + /// - `includedInDefaultResultSet`: Whether or not this property is returned in the default set of [Query.returningProperties]. Defaults to `true`. + /// - `autoincrement`: Whether or not this property should use an auto-incrementing scheme. Defaults to `false`. + /// - `validators`: A list of [ManagedValidator]s for this instance. Defaults to an empty list. + /// - `responseModel`: The [ResponseModel] associated with this property. + /// - `responseKey`: The [ResponseKey] associated with this property. ManagedAttributeDescription( super.entity, super.name, @@ -175,6 +298,17 @@ class ManagedAttributeDescription extends ManagedPropertyDescription { super.responseKey, }) : isPrimaryKey = primaryKey; + /// Initializes a new instance of [ManagedAttributeDescription] for a transient property. + /// + /// A transient property is a property that is not backed by a database column, but is still part of the [ManagedObject] model. + /// + /// Parameters: + /// - `entity`: The [ManagedEntity] that contains this property. + /// - `name`: The identifying name of this property. + /// - `type`: The value type of this property, indicating the Dart type and database column type. + /// - `declaredType`: The type of the variable that this property represents. + /// - `transientStatus`: The validity of this attribute as input, output or both. + /// - `responseKey`: The [ResponseKey] associated with this property. ManagedAttributeDescription.transient( super.entity, super.name, @@ -193,6 +327,28 @@ class ManagedAttributeDescription extends ManagedPropertyDescription { validators: [], ); + /// Creates a new instance of [ManagedAttributeDescription] with the provided parameters. + /// + /// This method is a factory method that simplifies the creation of [ManagedAttributeDescription] instances. + /// + /// Parameters: + /// - `entity`: The [ManagedEntity] that contains this property. + /// - `name`: The identifying name of this property. + /// - `type`: The value type of this property, indicating the Dart type and database column type. + /// - `transientStatus`: The validity of this attribute as input, output or both. + /// - `primaryKey`: Whether or not this attribute is the primary key for its [ManagedEntity]. Defaults to `false`. + /// - `defaultValue`: The default value for this attribute. Defaults to `null`. + /// - `unique`: Whether or not this property must be unique across all instances represented by `entity`. Defaults to `false`. + /// - `indexed`: Whether or not this property should be indexed by a [PersistentStore]. Defaults to `false`. + /// - `nullable`: Whether or not this property can be null. Defaults to `false`. + /// - `includedInDefaultResultSet`: Whether or not this property is returned in the default set of [Query.returningProperties]. Defaults to `true`. + /// - `autoincrement`: Whether or not this property should use an auto-incrementing scheme. Defaults to `false`. + /// - `validators`: A list of [ManagedValidator]s for this instance. Defaults to an empty list. + /// - `responseKey`: The [ResponseKey] associated with this property. + /// - `responseModel`: The [ResponseModel] associated with this property. + /// + /// Returns: + /// A new instance of [ManagedAttributeDescription] with the provided parameters. static ManagedAttributeDescription make( ManagedEntity entity, String name, @@ -228,25 +384,27 @@ class ManagedAttributeDescription extends ManagedPropertyDescription { ); } - /// Whether or not this attribute is the primary key for its [ManagedEntity]. + /// Indicates whether this attribute is the primary key for its [ManagedEntity]. /// /// Defaults to false. final bool isPrimaryKey; /// The default value for this attribute. /// - /// By default, null. This value is a String, so the underlying persistent store is responsible for parsing it. This allows for default values - /// that aren't constant values, such as database function calls. + /// By default, this property is `null`. This value is a `String`, so the underlying persistent store is responsible for parsing it. This allows for + /// default values that aren't constant values, such as database function calls. final String? defaultValue; - /// Whether or not this attribute is backed directly by the database. + /// Determines whether this attribute is backed directly by the database. /// /// If [transientStatus] is non-null, this value will be true. Otherwise, the attribute is backed by a database field/column. bool get isTransient => transientStatus != null; /// Contains lookup table for string value of an enumeration to the enumerated value. /// - /// Value is null when this attribute does not represent an enumerated type. + /// This property returns a map that maps the string representation of an enumeration value + /// to the actual enumeration value. This is used when dealing with enumerated values in + /// the context of a [ManagedAttributeDescription]. /// /// If `enum Options { option1, option2 }` then this map contains: /// @@ -260,11 +418,25 @@ class ManagedAttributeDescription extends ManagedPropertyDescription { /// The validity of a transient attribute as input, output or both. /// /// If this property is non-null, the attribute is transient (not backed by a database field/column). + /// The [Serialize] value indicates whether the attribute is available for input, output, or both. final Serialize? transientStatus; - /// Whether or not this attribute is represented by a Dart enum. + /// Determines whether this attribute is represented by a Dart enum. + /// + /// If the [enumerationValueMap] property is not empty, this attribute is considered an + /// enumerated value, meaning it is represented by a Dart enum. bool get isEnumeratedValue => enumerationValueMap.isNotEmpty; + /// Generates an [APISchemaObject] that represents the schema of this property for API documentation. + /// + /// This method creates an [APISchemaObject] that describes the schema of this property, including + /// information about its type, nullability, enumerations, and other metadata. + /// + /// Parameters: + /// - `context`: The [APIDocumentContext] that provides information about the current documentation context. + /// + /// Returns: + /// An [APISchemaObject] that represents the schema of this property. @override APISchemaObject documentSchemaObject(APIDocumentContext context) { final prop = ManagedPropertyDescription._typedSchemaObject(type!) @@ -310,6 +482,23 @@ class ManagedAttributeDescription extends ManagedPropertyDescription { return prop; } + /// Generates a string representation of the properties of this `ManagedPropertyDescription` instance. + /// + /// The resulting string includes information about the following properties: + /// - `isPrimaryKey`: Whether this property is the primary key for the associated `ManagedEntity`. + /// - `isTransient`: Whether this property is a transient property (i.e., not backed by a database column). + /// - `autoincrement`: Whether this property uses auto-incrementing for its values. + /// - `isUnique`: Whether this property must have unique values across all instances of the associated `ManagedEntity`. + /// - `defaultValue`: The default value for this property, if any. + /// - `isIndexed`: Whether this property is indexed in the database. + /// - `isNullable`: Whether this property can have a `null` value. + /// + /// The string representation is formatted as follows: + /// ``` + /// - | | Flags: ... + /// ``` + /// where `` is the name of the property, `` is the type of the property, and ``, ``, ..., `` are the flags + /// corresponding to the property's characteristics (e.g., `primary_key`, `transient`, `autoincrementing`, `unique`, `defaults to `, `indexed`, `nullable`, `required`). @override String toString() { final flagBuffer = StringBuffer(); @@ -340,6 +529,22 @@ class ManagedAttributeDescription extends ManagedPropertyDescription { return "- $name | $type | Flags: $flagBuffer"; } + /// Converts a value to a more primitive value according to this instance's definition. + /// + /// This method takes a Dart representation of a value and converts it to something that can + /// be used elsewhere (e.g. an HTTP body or database query). The conversion depends on the + /// type of this property. + /// + /// For `DateTime` values, the method converts the `DateTime` to an ISO 8601 string. + /// For enumerated values, the method converts the enum value to a string representing the enum name. + /// For `Document` values, the method extracts the data from the `Document` object. + /// For all other values, the method simply returns the original value. + /// + /// Parameters: + /// - [value]: The Dart representation of the value to be converted. + /// + /// Returns: + /// The converted primitive value. @override dynamic convertToPrimitiveValue(dynamic value) { if (value == null) { @@ -359,6 +564,24 @@ class ManagedAttributeDescription extends ManagedPropertyDescription { return value; } + /// Converts a value from a primitive value into a more complex value according to this instance's definition. + /// + /// This method takes a non-Dart representation of a value (e.g. an HTTP body or database query) + /// and turns it into a Dart representation. The conversion process depends on the type of this property. + /// + /// For `DateTime` values, the method parses the input string into a `DateTime` object. + /// For `double` values, the method converts the input number to a `double`. + /// For enumerated values, the method looks up the corresponding enum value using the `enumerationValueMap`. + /// For `Document` values, the method wraps the input value in a `Document` object. + /// For `List` and `Map` values, the method delegates the conversion to the `entity.runtime.dynamicConvertFromPrimitiveValue` method. + /// + /// If the input value is not compatible with the expected type, a `ValidationException` is thrown. + /// + /// Parameters: + /// - `value`: The non-Dart representation of the value to be converted. + /// + /// Returns: + /// The converted Dart representation of the value. @override dynamic convertFromPrimitiveValue(dynamic value) { if (value == null) { @@ -396,7 +619,33 @@ class ManagedAttributeDescription extends ManagedPropertyDescription { } /// Contains information for a relationship property of a [ManagedObject]. +/// +/// The `ManagedRelationshipDescription` class represents a relationship property of a [ManagedObject]. It contains information about the +/// destination entity, the delete rule, the relationship type, and the inverse key. This class is used to manage the data model and +/// provide information about relationship properties. class ManagedRelationshipDescription extends ManagedPropertyDescription { + /// Initializes a new instance of [ManagedRelationshipDescription]. + /// + /// This constructor creates a new instance of [ManagedRelationshipDescription], which represents a relationship property of a [ManagedObject]. + /// The constructor sets the properties of the [ManagedRelationshipDescription] instance, including the destination entity, delete rule, relationship type, + /// inverse key, and other metadata. + /// + /// Parameters: + /// - `entity`: The [ManagedEntity] that contains this property. + /// - `name`: The identifying name of this property. + /// - `type`: The value type of this property, indicating the Dart type and database column type. + /// - `declaredType`: The type of the variable that this property represents. + /// - `destinationEntity`: The [ManagedEntity] that represents the destination of this relationship. + /// - `deleteRule`: The delete rule for this relationship. + /// - `relationshipType`: The type of relationship (e.g., belongs to, has one, has many). + /// - `inverseKey`: The name of the [ManagedRelationshipDescription] on the `destinationEntity` that represents the inverse of this relationship. + /// - `unique`: Whether or not this property must be unique across all instances represented by `entity`. Defaults to `false`. + /// - `indexed`: Whether or not this property should be indexed by a [PersistentStore]. Defaults to `false`. + /// - `nullable`: Whether or not this property can be null. Defaults to `false`. + /// - `includedInDefaultResultSet`: Whether or not this property is returned in the default set of [Query.returningProperties]. Defaults to `true`. + /// - `validators`: A list of [ManagedValidator]s for this instance. Defaults to an empty list. + /// - `responseModel`: The [ResponseModel] associated with this property. + /// - `responseKey`: The [ResponseKey] associated with this property. ManagedRelationshipDescription( super.entity, super.name, @@ -415,6 +664,28 @@ class ManagedRelationshipDescription extends ManagedPropertyDescription { super.responseKey, }); + /// Creates a new instance of [ManagedRelationshipDescription] with the provided parameters. + /// + /// This method is a factory method that simplifies the creation of [ManagedRelationshipDescription] instances. + /// + /// Parameters: + /// - `entity`: The [ManagedEntity] that contains this property. + /// - `name`: The identifying name of this property. + /// - `type`: The value type of this property, indicating the Dart type and database column type. + /// - `destinationEntity`: The [ManagedEntity] that represents the destination of this relationship. + /// - `deleteRule`: The delete rule for this relationship. + /// - `relationshipType`: The type of relationship (e.g., belongs to, has one, has many). + /// - `inverseKey`: The name of the [ManagedRelationshipDescription] on the `destinationEntity` that represents the inverse of this relationship. + /// - `unique`: Whether or not this property must be unique across all instances represented by `entity`. Defaults to `false`. + /// - `indexed`: Whether or not this property should be indexed by a [PersistentStore]. Defaults to `false`. + /// - `nullable`: Whether or not this property can be null. Defaults to `false`. + /// - `includedInDefaultResultSet`: Whether or not this property is returned in the default set of [Query.returningProperties]. Defaults to `true`. + /// - `validators`: A list of [ManagedValidator]s for this instance. Defaults to an empty list. + /// - `responseKey`: The [ResponseKey] associated with this property. + /// - `responseModel`: The [ResponseModel] associated with this property. + /// + /// Returns: + /// A new instance of [ManagedRelationshipDescription] with the provided parameters. static ManagedRelationshipDescription make( ManagedEntity entity, String name, @@ -450,26 +721,78 @@ class ManagedRelationshipDescription extends ManagedPropertyDescription { ); } - /// The entity that this relationship's instances are represented by. + /// The [ManagedEntity] that represents the destination of this relationship. + /// + /// This property holds a reference to the [ManagedEntity] that describes the model + /// that the objects on the other end of this relationship belong to. This is used + /// to ensure that the values assigned to this relationship property are compatible + /// with the expected model. final ManagedEntity destinationEntity; /// The delete rule for this relationship. + /// + /// The delete rule determines what happens to the related objects when the object + /// containing this relationship is deleted. The possible values are: + /// + /// - `DeleteRule.cascade`: When the object is deleted, all related objects are also deleted. + /// - `DeleteRule.restrict`: When the object is deleted, the operation will fail if there are any related objects. + /// - `DeleteRule.nullify`: When the object is deleted, the foreign key values in the related objects will be set to `null`. + /// - `DeleteRule.setDefault`: When the object is deleted, the foreign key values in the related objects will be set to their default values. final DeleteRule? deleteRule; - /// The type of relationship. + /// The type of relationship represented by this [ManagedRelationshipDescription]. + /// + /// The relationship type can be one of the following: + /// - `ManagedRelationshipType.belongsTo`: This property represents a "belongs to" relationship, where the object containing this property + /// belongs to another object. + /// - `ManagedRelationshipType.hasOne`: This property represents a "has one" relationship, where the object containing this property + /// has a single related object. + /// - `ManagedRelationshipType.hasMany`: This property represents a "has many" relationship, where the object containing this property + /// has a set of related objects. final ManagedRelationshipType relationshipType; - /// The name of the [ManagedRelationshipDescription] on [destinationEntity] that represents the inverse of this relationship. + /// The [ManagedRelationshipDescription] on [destinationEntity] that represents the inverse of this relationship. + /// + /// This property holds the name of the [ManagedRelationshipDescription] on the [destinationEntity] that represents the inverse + /// of this relationship. This information is used to ensure that the relationships between objects are properly defined and + /// navigable in both directions. final String inverseKey; - /// The [ManagedRelationshipDescription] on [destinationEntity] that represents the inverse of this relationship. + /// Gets the [ManagedRelationshipDescription] on [destinationEntity] that represents the inverse of this relationship. + /// + /// This property returns the [ManagedRelationshipDescription] instance on the [destinationEntity] that represents the + /// inverse of the current relationship. The inverse relationship is specified by the [inverseKey] property. + /// + /// This method is used to navigate the relationship in the opposite direction, allowing you to access the related + /// objects from the other side of the relationship. + /// + /// Returns: + /// The [ManagedRelationshipDescription] that represents the inverse of this relationship, or `null` if no inverse + /// relationship is defined. ManagedRelationshipDescription? get inverse => destinationEntity.relationships[inverseKey]; - /// Whether or not this relationship is on the belonging side. + /// Indicates whether this relationship is on the belonging side of the relationship. + /// + /// This property returns `true` if the `relationshipType` of this `ManagedRelationshipDescription` is + /// `ManagedRelationshipType.belongsTo`, which means that the object containing this property "belongs to" + /// another object. If the `relationshipType` is not `belongsTo`, this property returns `false`. bool get isBelongsTo => relationshipType == ManagedRelationshipType.belongsTo; - /// Whether or not a the argument can be assigned to this property. + /// Determines whether the provided Dart value can be assigned to this property. + /// + /// This method checks if the given `dartValue` is compatible with the type of this property. + /// For relationships with a 'has many' type, the method checks if the `dartValue` is a list of + /// [ManagedObject] instances that belong to the destination entity. For other relationship + /// types, the method checks if the `dartValue` is a [ManagedObject] instance that belongs + /// to the destination entity. + /// + /// Parameters: + /// - `dartValue`: The Dart value to be checked for assignment compatibility. + /// + /// Returns: + /// - `true` if the `dartValue` can be assigned to this property. + /// - `false` otherwise. @override bool isAssignableWith(dynamic dartValue) { if (relationshipType == ManagedRelationshipType.hasMany) { @@ -478,6 +801,31 @@ class ManagedRelationshipDescription extends ManagedPropertyDescription { return destinationEntity.runtime.isValueInstanceOf(dartValue); } + /// Converts a value to a more primitive value according to this instance's definition. + /// + /// This method takes a Dart representation of a value and converts it to something that can + /// be used elsewhere (e.g. an HTTP body or database query). The conversion process depends + /// on the type of the relationship. + /// + /// For relationship properties with a "has many" type, the method converts the `ManagedSet` + /// instance to a list of maps, where each map represents the associated `ManagedObject` + /// instances. + /// + /// For relationship properties with a "belongs to" type, the method checks if only the + /// primary key of the associated `ManagedObject` is being fetched. If so, it returns a + /// map containing only the primary key value. Otherwise, it returns the full map + /// representation of the associated `ManagedObject`. + /// + /// If the provided `value` is `null`, the method returns `null`. + /// + /// If the provided `value` is not a `ManagedSet` or `ManagedObject`, the method throws a + /// `StateError` with a message indicating the invalid relationship assignment. + /// + /// Parameters: + /// - `value`: The Dart representation of the value to be converted. + /// + /// Returns: + /// The converted primitive value. @override dynamic convertToPrimitiveValue(dynamic value) { if (value is ManagedSet) { @@ -504,6 +852,26 @@ class ManagedRelationshipDescription extends ManagedPropertyDescription { ); } + /// Converts a value from a primitive value into a more complex value according to this instance's definition. + /// + /// This method takes a non-Dart representation of a value (e.g. an HTTP body or database query) + /// and turns it into a Dart representation. The conversion process depends on the type of the relationship. + /// + /// For relationship properties with a "belongs to" or "has one" type, the method creates a new instance of the + /// [ManagedObject] associated with the destination entity, and populates it with the data from the provided map. + /// + /// For relationship properties with a "has many" type, the method creates a [ManagedSet] instance, and populates + /// it with new [ManagedObject] instances created from the provided list of maps. + /// + /// If the input value is `null`, the method returns `null`. + /// + /// If the input value is not a map or list, as expected for the relationship type, a [ValidationException] is thrown. + /// + /// Parameters: + /// - `value`: The non-Dart representation of the value to be converted. + /// + /// Returns: + /// The converted Dart representation of the value. @override dynamic convertFromPrimitiveValue(dynamic value) { if (value == null) { @@ -538,6 +906,17 @@ class ManagedRelationshipDescription extends ManagedPropertyDescription { return destinationEntity.setOf(value.map(instantiator)); } + /// Generates an [APISchemaObject] that represents the schema of this relationship property for API documentation. + /// + /// This method creates an [APISchemaObject] that describes the schema of this relationship property, including + /// information about the type of relationship (hasMany, hasOne, or belongsTo), the related object schema, and + /// whether the property is read-only and nullable. + /// + /// Parameters: + /// - `context`: The [APIDocumentContext] that provides information about the current documentation context. + /// + /// Returns: + /// An [APISchemaObject] that represents the schema of this relationship property. @override APISchemaObject documentSchemaObject(APIDocumentContext context) { final relatedType = @@ -560,6 +939,18 @@ class ManagedRelationshipDescription extends ManagedPropertyDescription { ..title = name; } + /// Generates a string representation of the properties of this `ManagedRelationshipDescription` instance. + /// + /// The resulting string includes information about the following properties: + /// - `name`: The identifying name of this property. + /// - `destinationEntity`: The name of the `ManagedEntity` that represents the destination of this relationship. + /// - `relationshipType`: The type of relationship (e.g., `belongs to`, `has one`, `has many`). + /// - `inverseKey`: The name of the `ManagedRelationshipDescription` on the `destinationEntity` that represents the inverse of this relationship. + /// + /// The string representation is formatted as follows: + /// ``` + /// - -> '' | Type: | Inverse: + /// ``` @override String toString() { var relTypeString = "has-one"; diff --git a/packages/database/lib/src/managed/relationship_type.dart b/packages/database/lib/src/managed/relationship_type.dart index 1166e7b..5b7bf92 100644 --- a/packages/database/lib/src/managed/relationship_type.dart +++ b/packages/database/lib/src/managed/relationship_type.dart @@ -1,2 +1,18 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + /// The possible database relationships. +/// +/// This enum represents the different types of relationships that can exist between +/// database entities. The available relationship types are: +/// +/// - `hasOne`: A one-to-one relationship, where one entity has exactly one related entity. +/// - `hasMany`: A one-to-many relationship, where one entity can have multiple related entities. +/// - `belongsTo`: A many-to-one relationship, where multiple entities can belong to a single parent entity. enum ManagedRelationshipType { hasOne, hasMany, belongsTo } diff --git a/packages/database/lib/src/managed/set.dart b/packages/database/lib/src/managed/set.dart index 2d0e1e5..4786213 100644 --- a/packages/database/lib/src/managed/set.dart +++ b/packages/database/lib/src/managed/set.dart @@ -1,5 +1,13 @@ -import 'dart:collection'; +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import 'dart:collection'; import 'package:protevus_database/src/managed/managed.dart'; /// Instances of this type contain zero or more instances of [ManagedObject] and represent has-many relationships. @@ -22,48 +30,78 @@ import 'package:protevus_database/src/managed/managed.dart'; class ManagedSet extends Object with ListMixin { /// Creates an empty [ManagedSet]. + /// + /// This constructor initializes a new [ManagedSet] instance with an empty internal list. ManagedSet() { _innerValues = []; } /// Creates a [ManagedSet] from an [Iterable] of [InstanceType]s. + /// + /// This constructor initializes a new [ManagedSet] instance with the elements of the provided [Iterable]. ManagedSet.from(Iterable items) { _innerValues = items.toList(); } /// Creates a [ManagedSet] from an [Iterable] of [dynamic]s. + /// + /// This constructor initializes a new [ManagedSet] instance with the elements of the provided [Iterable] of [dynamic]s. + /// The elements are converted to the appropriate [InstanceType] using [List.from]. ManagedSet.fromDynamic(Iterable items) { _innerValues = List.from(items); } + /// The internal list that stores the elements of this [ManagedSet]. late final List _innerValues; - /// The number of elements in this set. + /// The number of elements in this [ManagedSet]. + /// + /// This property returns the number of elements in the internal list that stores the elements of this [ManagedSet]. @override int get length => _innerValues.length; + /// Sets the length of the internal list that stores the elements of this [ManagedSet]. + /// + /// This setter allows you to change the length of the internal list that stores the elements of this [ManagedSet]. + /// If the new length is greater than the current length, the list is extended and the new elements are initialized to `null`. + /// If the new length is less than the current length, the list is truncated to the new length. @override set length(int newLength) { _innerValues.length = newLength; } - /// Adds an [InstanceType] to this set. + /// Adds an [InstanceType] object to this [ManagedSet]. + /// + /// This method adds the provided [InstanceType] object to the internal list of this [ManagedSet]. + /// The length of the [ManagedSet] is increased by 1, and the new element is appended to the end of the list. @override void add(InstanceType item) { _innerValues.add(item); } - /// Adds an [Iterable] of [InstanceType] to this set. + /// Adds all the elements of the provided [Iterable] of [InstanceType] to this [ManagedSet]. + /// + /// This method adds all the elements of the provided [Iterable] to the internal list of this [ManagedSet]. + /// The length of the [ManagedSet] is increased by the number of elements in the [Iterable], and the new elements + /// are appended to the end of the list. @override void addAll(Iterable items) { _innerValues.addAll(items); } /// Retrieves an [InstanceType] from this set by an index. + /// + /// This overloaded index operator allows you to access the elements of the internal list + /// that stores the elements of this [ManagedSet] using an integer index. The element + /// at the specified index is returned. @override InstanceType operator [](int index) => _innerValues[index]; - /// Set an [InstanceType] in this set by an index. + /// Sets the [InstanceType] object at the specified [index] in this [ManagedSet]. + /// + /// This overloaded index assignment operator allows you to assign a new [InstanceType] object to the + /// element at the specified [index] in the internal list that stores the elements of this [ManagedSet]. + /// If the [index] is out of bounds, an [RangeError] will be thrown. @override void operator []=(int index, InstanceType value) { _innerValues[index] = value; diff --git a/packages/database/lib/src/managed/type.dart b/packages/database/lib/src/managed/type.dart index 933b8cc..3d2fc56 100644 --- a/packages/database/lib/src/managed/type.dart +++ b/packages/database/lib/src/managed/type.dart @@ -1,6 +1,18 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_database/src/managed/managed.dart'; /// Possible data types for [ManagedEntity] attributes. +/// +/// This enum represents the different data types that can be used for attributes in a [ManagedEntity]. +/// Each enum value corresponds to a specific Dart data type that will be used to represent the attribute. enum ManagedPropertyType { /// Represented by instances of [int]. integer, @@ -30,13 +42,27 @@ enum ManagedPropertyType { document } -/// Complex type storage for [ManagedEntity] attributes. +/// Represents complex data types for attributes in a [ManagedEntity]. +/// +/// This class provides a way to represent complex data types, such as maps, lists, and enumerations, that can be used as +/// attributes in a [ManagedEntity]. It encapsulates information about the type, including the primitive kind, the type +/// of elements in the case of collections, and whether the type is an enumeration. +/// +/// The [ManagedType] class is used internally by the Protevus database management system to handle the storage and +/// retrieval of complex data types in the database. class ManagedType { - /// Creates a new instance. + /// Creates a new instance of [ManagedType] with the provided parameters. /// /// [type] must be representable by [ManagedPropertyType]. ManagedType(this.type, this.kind, this.elements, this.enumerationMap); + /// Creates a new instance of [ManagedType] with the provided parameters. + /// + /// [kind] is the primitive type of the managed property. + /// [elements] is the type of the elements in the case of a collection (map or list) property. + /// [enumerationMap] is a map of the enum options and their corresponding Dart enum types, in the case of an enumerated property. + /// + /// This method is a convenience constructor for creating [ManagedType] instances with the appropriate parameters. static ManagedType make( ManagedPropertyType kind, ManagedType? elements, @@ -47,25 +73,48 @@ class ManagedType { /// The primitive kind of this type. /// - /// All types have a kind. If kind is a map or list, it will also have [elements]. + /// All types have a kind. If [kind] is a map or list, it will also have [elements] to specify the type of the map keys or list elements. final ManagedPropertyType kind; - /// The primitive kind of each element of this type. + /// The type of the elements in this managed property. /// /// If [kind] is a collection (map or list), this value stores the type of each element in the collection. /// Keys of map types are always [String]. final ManagedType? elements; - /// Dart representation of this type. + /// The Dart type represented by this [ManagedType] instance. final Type type; - /// Whether this is an enum type. + /// Whether this [ManagedType] instance represents an enumerated type. + /// + /// This property returns `true` if the `enumerationMap` property is not empty, indicating that this type represents an enumeration. Otherwise, it returns `false`. bool get isEnumerated => enumerationMap.isNotEmpty; /// For enumerated types, this is a map of the name of the option to its Dart enum type. + /// + /// This property provides a way to associate the string representation of an enumeration value with its corresponding + /// Dart enum type. It is used in the context of a [ManagedType] instance to represent an enumerated property in a + /// [ManagedEntity]. + /// + /// The keys of this map are the string representations of the enum options, and the values are the corresponding + /// Dart enum types. final Map enumerationMap; - /// Whether [dartValue] can be assigned to properties with this type. + /// Checks whether the provided [dartValue] can be assigned to properties with this [ManagedType]. + /// + /// This method examines the [kind] of the [ManagedType] and determines whether the provided [dartValue] is compatible + /// with the expected data type. + /// + /// If the [dartValue] is `null`, this method will return `true`, as null can be assigned to any property. + /// + /// For each specific [ManagedPropertyType], the method checks the type of the [dartValue] and returns `true` if it + /// matches the expected type, and `false` otherwise. + /// + /// For [ManagedPropertyType.string], if the [enumerationMap] is not empty, the method checks whether the [dartValue] + /// is one of the enum values in the map. + /// + /// @param dartValue The value to be checked for assignment compatibility. + /// @return `true` if the [dartValue] can be assigned to properties with this [ManagedType], `false` otherwise. bool isAssignableWith(dynamic dartValue) { if (dartValue == null) { return true; @@ -98,31 +147,90 @@ class ManagedType { } } + /// Returns a string representation of the [ManagedPropertyType] instance. + /// + /// The string representation is simply the name of the [ManagedPropertyType] enum value. + /// This method is useful for logging or debugging purposes, as it provides a human-readable + /// representation of the property type. + /// + /// Example usage: + /// ```dart + /// ManagedPropertyType type = ManagedPropertyType.integer; + /// print(type.toString()); // Output: "integer" + /// ``` @override String toString() { return "$kind"; } + /// Returns a list of Dart types that are supported by the Protevus database management system. + /// + /// The supported Dart types are: + /// - `String`: Represents a string of text. + /// - `DateTime`: Represents a specific date and time. + /// - `bool`: Represents a boolean value (true or false). + /// - `int`: Represents an integer number. + /// - `double`: Represents a floating-point number. + /// - `Document`: Represents a complex data structure that can be stored in the database. + /// + /// This list of supported types is used internally by the Protevus database management system to ensure that + /// the data being stored in the database is compatible with the expected data types. static List get supportedDartTypes { return [String, DateTime, bool, int, double, Document]; } + /// Returns the [ManagedPropertyType] for integer properties. + /// + /// This property provides a convenient way to access the [ManagedPropertyType.integer] value, + /// which represents integer properties in a [ManagedEntity]. static ManagedPropertyType get integer => ManagedPropertyType.integer; + /// Returns the [ManagedPropertyType] for big integer properties. + /// + /// This property provides a convenient way to access the [ManagedPropertyType.bigInteger] value, + /// which represents big integer properties in a [ManagedEntity]. static ManagedPropertyType get bigInteger => ManagedPropertyType.bigInteger; + /// Returns the [ManagedPropertyType] for string properties. + /// + /// This property provides a convenient way to access the [ManagedPropertyType.string] value, + /// which represents string properties in a [ManagedEntity]. static ManagedPropertyType get string => ManagedPropertyType.string; + /// Returns the [ManagedPropertyType] for datetime properties. + /// + /// This property provides a convenient way to access the [ManagedPropertyType.datetime] value, + /// which represents datetime properties in a [ManagedEntity]. static ManagedPropertyType get datetime => ManagedPropertyType.datetime; + /// Returns the [ManagedPropertyType] for boolean properties. + /// + /// This property provides a convenient way to access the [ManagedPropertyType.boolean] value, + /// which represents boolean properties in a [ManagedEntity]. static ManagedPropertyType get boolean => ManagedPropertyType.boolean; + /// Returns the [ManagedPropertyType] for double precision properties. + /// + /// This property provides a convenient way to access the [ManagedPropertyType.doublePrecision] value, + /// which represents double precision properties in a [ManagedEntity]. static ManagedPropertyType get doublePrecision => ManagedPropertyType.doublePrecision; + /// Returns the [ManagedPropertyType] for map properties. + /// + /// This property provides a convenient way to access the [ManagedPropertyType.map] value, + /// which represents map properties in a [ManagedEntity]. static ManagedPropertyType get map => ManagedPropertyType.map; + /// Returns the [ManagedPropertyType] for list properties. + /// + /// This property provides a convenient way to access the [ManagedPropertyType.list] value, + /// which represents list properties in a [ManagedEntity]. static ManagedPropertyType get list => ManagedPropertyType.list; + /// Returns the [ManagedPropertyType] for document properties. + /// + /// This property provides a convenient way to access the [ManagedPropertyType.document] value, + /// which represents document properties in a [ManagedEntity]. static ManagedPropertyType get document => ManagedPropertyType.document; } diff --git a/packages/database/lib/src/managed/validation/impl.dart b/packages/database/lib/src/managed/validation/impl.dart index f9e25f3..a4ecb16 100644 --- a/packages/database/lib/src/managed/validation/impl.dart +++ b/packages/database/lib/src/managed/validation/impl.dart @@ -1,7 +1,31 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_database/src/managed/managed.dart'; +/// Represents the different types of validations that can be performed on an input. +/// +/// - `regex`: Validate the input using a regular expression pattern. +/// - `comparison`: Validate the input using a comparison operator and a value. +/// - `length`: Validate the length of the input. +/// - `present`: Ensure the input is not null or empty. +/// - `absent`: Ensure the input is null or empty. +/// - `oneOf`: Ensure the input is one of the specified values. enum ValidateType { regex, comparison, length, present, absent, oneOf } +/// Represents the different comparison operators that can be used in a validation expression. +/// +/// - `equalTo`: Ensures the input is equal to the specified value. +/// - `lessThan`: Ensures the input is less than the specified value. +/// - `lessThanEqualTo`: Ensures the input is less than or equal to the specified value. +/// - `greaterThan`: Ensures the input is greater than the specified value. +/// - `greaterThanEqualTo`: Ensures the input is greater than or equal to the specified value. enum ValidationOperator { equalTo, lessThan, @@ -10,15 +34,55 @@ enum ValidationOperator { greaterThanEqualTo } +/// Represents a validation expression that can be used to validate an input value. +/// +/// The `ValidationExpression` class has two properties: +/// +/// - `operator`: The comparison operator to be used in the validation. +/// - `value`: The value to be compared against the input. +/// +/// The `compare` method is used to perform the validation and add any errors to the provided `ValidationContext`. class ValidationExpression { + /// Initializes a new instance of the [ValidationExpression] class. + /// + /// The [operator] parameter specifies the comparison operator to be used in the validation. + /// The [value] parameter specifies the value to be compared against the input. ValidationExpression(this.operator, this.value); + /// The comparison operator to be used in the validation. final ValidationOperator operator; + + /// The value to be compared against the input during the validation process. dynamic value; + /// Compares the provided input value against the value specified in the [ValidationExpression]. + /// + /// The comparison is performed based on the [ValidationOperator] specified in the [ValidationExpression]. + /// If the comparison fails, an error message is added to the provided [ValidationContext]. + /// + /// Parameters: + /// - [context]: The [ValidationContext] to which any errors will be added. + /// - [input]: The value to be compared against the [ValidationExpression] value. + /// + /// Throws: + /// - [ClassCastException]: If the [value] property of the [ValidationExpression] is not a [Comparable]. void compare(ValidationContext context, dynamic input) { + /// Converts the [value] property of the [ValidationExpression] to a [Comparable] type, or sets it to `null` if the conversion fails. + /// + /// This step is necessary because the [compare] method requires the [value] to be a [Comparable] in order to perform the comparison. final comparisonValue = value as Comparable?; + /// Compares the provided input value against the value specified in the [ValidationExpression]. + /// + /// The comparison is performed based on the [ValidationOperator] specified in the [ValidationExpression]. + /// If the comparison fails, an error message is added to the provided [ValidationContext]. + /// + /// Parameters: + /// - [context]: The [ValidationContext] to which any errors will be added. + /// - [input]: The value to be compared against the [ValidationExpression] value. + /// + /// Throws: + /// - [ClassCastException]: If the [value] property of the [ValidationExpression] is not a [Comparable]. switch (operator) { case ValidationOperator.equalTo: { diff --git a/packages/database/lib/src/managed/validation/managed.dart b/packages/database/lib/src/managed/validation/managed.dart index 8987fe2..7e8b756 100644 --- a/packages/database/lib/src/managed/validation/managed.dart +++ b/packages/database/lib/src/managed/validation/managed.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_database/src/managed/managed.dart'; import 'package:protevus_database/src/managed/validation/impl.dart'; import 'package:protevus_database/src/query/query.dart'; @@ -6,6 +15,10 @@ import 'package:protevus_database/src/query/query.dart'; /// /// Instances of this type are created during [ManagedDataModel] compilation. class ManagedValidator { + /// Constructs a [ManagedValidator] instance with the specified [definition] and [state]. + /// + /// The [definition] parameter contains the metadata associated with this instance, while + /// the [state] parameter holds a dynamic value that can be used during the validation process. ManagedValidator(this.definition, this.state); /// Executes all [Validate]s for [object]. @@ -81,17 +94,52 @@ class ManagedValidator { } /// The property being validated. + /// + /// This property represents the [ManagedPropertyDescription] that is being + /// validated by the current instance of [ManagedValidator]. It is used to + /// retrieve information about the property, such as its name, type, and + /// relationship details. ManagedPropertyDescription? property; /// The metadata associated with this instance. + /// + /// The `definition` property contains the metadata associated with this instance of `ManagedValidator`. + /// This metadata is used to define the validation rules that will be applied to the properties + /// of a `ManagedObject` during an insert or update operation. final Validate definition; + /// The dynamic state associated with this validator. + /// + /// This property holds a dynamic value that can be used during the validation process. + /// The state is provided when the [ManagedValidator] is constructed and can be used + /// by the validation logic to customize the validation behavior. final dynamic state; + /// Validates the property according to the validation rules defined in the [definition] property. + /// + /// This method is called by the [run] method of the [ManagedValidator] class to perform the actual + /// validation of a property value. The [context] parameter is used to store the validation results, + /// and the [value] parameter is the value of the property being validated. + /// + /// The validation logic is defined in the [definition] property, which is an instance of the [Validate] + /// class. This class contains the metadata that describes the validation rules to be applied to the + /// property. void validate(ValidationContext context, dynamic value) { definition.validate(context, value); } + /// Returns a string representation of the given validation event. + /// + /// This method is a helper function that takes a [Validating] event and + /// returns a string describing the event. + /// + /// Parameters: + /// - `op`: The [Validating] event to be described. + /// + /// Returns: + /// A string representing the given validation event. The possible return + /// values are "insert", "update", or "unknown" if the event is not + /// recognized. static String _getEventName(Validating op) { switch (op) { case Validating.insert: diff --git a/packages/database/lib/src/managed/validation/metadata.dart b/packages/database/lib/src/managed/validation/metadata.dart index dd2e7dd..153d405 100644 --- a/packages/database/lib/src/managed/validation/metadata.dart +++ b/packages/database/lib/src/managed/validation/metadata.dart @@ -1,9 +1,21 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_openapi/documentable.dart'; import 'package:protevus_database/db.dart'; import 'package:protevus_database/src/managed/validation/impl.dart'; import 'package:protevus_openapi/v3.dart'; /// Types of operations [ManagedValidator]s will be triggered for. +/// +/// - [update]: The validation is triggered during an update operation. +/// - [insert]: The validation is triggered during an insert operation. enum Validating { update, insert } /// Information about a validation being performed. @@ -52,6 +64,7 @@ class ValidationContext { class ValidateCompilationError extends Error { ValidateCompilationError(this.reason); + /// The reason for the [ValidateCompilationError]. final String reason; } @@ -79,7 +92,9 @@ class ValidateCompilationError extends Error { class Validate { /// Invoke this constructor when creating custom subclasses. /// - /// This constructor is used so that subclasses can pass [onUpdate] and [onInsert]. + /// This constructor is used so that subclasses can pass [onUpdate] and [onInsert] values to control when + /// the validation is performed. For example: + /// /// Example: /// class CustomValidate extends Validate { /// CustomValidate({bool onUpdate: true, bool onInsert: true}) @@ -104,6 +119,20 @@ class Validate { _equalTo = null, type = null; + /// A private constructor used to create instances of the [Validate] class. + /// + /// This constructor is used by the various named constructors of the [Validate] class to set the instance + /// variables with the provided values. + /// + /// - [onUpdate]: Whether the validation should be performed during update operations. + /// - [onInsert]: Whether the validation should be performed during insert operations. + /// - [validator]: The type of validation to perform. + /// - [value]: A value used by the validation. + /// - [greaterThan]: A value to compare the input value against using the "greater than" operator. + /// - [greaterThanEqualTo]: A value to compare the input value against using the "greater than or equal to" operator. + /// - [equalTo]: A value to compare the input value against using the "equal to" operator. + /// - [lessThan]: A value to compare the input value against using the "less than" operator. + /// - [lessThanEqualTo]: A value to compare the input value against using the "less than or equal to" operator. const Validate._({ bool onUpdate = true, bool onInsert = true, @@ -292,21 +321,67 @@ class Validate { /// A validator that ensures a value cannot be modified after insertion. /// - /// This is equivalent to `Validate.absent(onUpdate: true, onInsert: false). + /// This is equivalent to `Validate.absent(onUpdate: true, onInsert: false)`. + /// + /// This validator is used to ensure that a property, once set during the initial + /// insertion of a record, cannot be updated. For example, you might use this + /// validator on a `dateCreated` property to ensure that the creation date + /// cannot be changed after the record is inserted. const Validate.constant() : this.absent(onUpdate: true, onInsert: false); /// Whether or not this validation is checked on update queries. + /// + /// This property determines whether the validation will be performed during update operations on the database. + /// If `true`, the validation will be executed during update queries. If `false`, the validation will be skipped + /// during update queries. final bool runOnUpdate; /// Whether or not this validation is checked on insert queries. + /// + /// This property determines whether the validation will be performed during insert operations on the database. + /// If `true`, the validation will be executed during insert queries. If `false`, the validation will be skipped + /// during insert queries. final bool runOnInsert; + /// The value associated with the validator. + /// + /// The meaning of this value depends on the type of validator. For example, for a + /// [Validate.matches] validator, this value would be the regular expression pattern to + /// match against. For a [Validate.oneOf] validator, this value would be the list of + /// allowed values. final dynamic _value; + + /// The greater than value for the comparison validation. final Comparable? _greaterThan; + + /// The greater than or equal to value for the comparison validation. final Comparable? _greaterThanEqualTo; + + /// The value to compare the input value against using the "equal to" operator. + /// + /// This value is used in the [_comparisonCompiler] method to create a [ValidationExpression] + /// with the [ValidationOperator.equalTo] operator. final Comparable? _equalTo; + + /// The "less than" value for the comparison validation. final Comparable? _lessThan; + + /// The "less than or equal to" value for the comparison validation. + /// + /// This value is used in the [_comparisonCompiler] method to create a [ValidationExpression] + /// with the [ValidationOperator.lessThanEqualTo] operator. final Comparable? _lessThanEqualTo; + + /// The type of validation to be performed. + /// + /// This can be one of the following values: + /// + /// - `ValidateType.absent`: The property must not be present in the update or insert query. + /// - `ValidateType.present`: The property must be present in the update or insert query. + /// - `ValidateType.oneOf`: The property value must be one of the values in the provided list. + /// - `ValidateType.comparison`: The property value must meet the comparison conditions specified. + /// - `ValidateType.regex`: The property value must match the provided regular expression. + /// - `ValidateType.length`: The length of the property value must meet the specified length conditions. final ValidateType? type; /// Subclasses override this method to perform any one-time initialization tasks and check for correctness. @@ -422,10 +497,95 @@ class Validate { /// /// Used during documentation process. When creating custom validator subclasses, override this method /// to modify [object] for any constraints the validator imposes. + /// This method is used during the documentation process. When creating custom validator subclasses, + /// override this method to modify the [object] parameter for any constraints the validator imposes. + /// + /// The constraints added to the [APISchemaObject] depend on the type of validator: + /// + /// - For `ValidateType.regex` validators, the `pattern` property of the [APISchemaObject] is set to the + /// regular expression pattern specified in the validator. + /// - For `ValidateType.comparison` validators, the `minimum`, `maximum`, `exclusiveMinimum`, and + /// `exclusiveMaximum` properties of the [APISchemaObject] are set based on the comparison values + /// specified in the validator. + /// - For `ValidateType.length` validators, the `minLength`, `maxLength`, and `maximum` properties + /// of the [APISchemaObject] are set based on the length-related values specified in the validator. + /// - For `ValidateType.present` and `ValidateType.absent` validators, no constraints are added to the + /// [APISchemaObject]. + /// - For `ValidateType.oneOf` validators, the `enumerated` property of the [APISchemaObject] is set + /// to the list of allowed values specified in the validator. + /// + /// @param context The [APIDocumentContext] being used to document the API. + /// @param object The [APISchemaObject] to which constraints should be added. void constrainSchemaObject( + /// Adds constraints to an [APISchemaObject] imposed by this validator. + /// + /// This method is used during the documentation process. When creating custom validator subclasses, + /// override this method to modify the [object] parameter for any constraints the validator imposes. + /// + /// The constraints added to the [APISchemaObject] depend on the type of validator: + /// + /// - For `ValidateType.regex` validators, the `pattern` property of the [APISchemaObject] is set to the + /// regular expression pattern specified in the validator. + /// - For `ValidateType.comparison` validators, the `minimum`, `maximum`, `exclusiveMinimum`, and + /// `exclusiveMaximum` properties of the [APISchemaObject] are set based on the comparison values + /// specified in the validator. + /// - For `ValidateType.length` validators, the `minLength`, `maxLength`, and `maximum` properties + /// of the [APISchemaObject] are set based on the length-related values specified in the validator. + /// - For `ValidateType.present` and `ValidateType.absent` validators, no constraints are added to the + /// [APISchemaObject]. + /// - For `ValidateType.oneOf` validators, the `enumerated` property of the [APISchemaObject] is set + /// to the list of allowed values specified in the validator. + /// + /// @param context The [APIDocumentContext] being used to document the API. + /// @param object The [APISchemaObject] to which constraints should be added. APIDocumentContext context, + + /// Adds constraints to an [APISchemaObject] imposed by this validator. + /// + /// This method is used during the documentation process. When creating custom validator subclasses, + /// override this method to modify the [object] parameter for any constraints the validator imposes. + /// + /// The constraints added to the [APISchemaObject] depend on the type of validator: + /// + /// - For `ValidateType.regex` validators, the `pattern` property of the [APISchemaObject] is set to the + /// regular expression pattern specified in the validator. + /// - For `ValidateType.comparison` validators, the `minimum`, `maximum`, `exclusiveMinimum`, and + /// `exclusiveMaximum` properties of the [APISchemaObject] are set based on the comparison values + /// specified in the validator. + /// - For `ValidateType.length` validators, the `minLength`, `maxLength`, and `maximum` properties + /// of the [APISchemaObject] are set based on the length-related values specified in the validator. + /// - For `ValidateType.present` and `ValidateType.absent` validators, no constraints are added to the + /// [APISchemaObject]. + /// - For `ValidateType.oneOf` validators, the `enumerated` property of the [APISchemaObject] is set + /// to the list of allowed values specified in the validator. + /// + /// @param context The [APIDocumentContext] being used to document the API. + /// @param object The [APISchemaObject] to which constraints should be added. APISchemaObject object, ) { + /// Adds constraints to an [APISchemaObject] imposed by this validator. + /// + /// This method is used during the documentation process. When creating custom validator subclasses, + /// override this method to modify the [object] parameter for any constraints the validator imposes. + /// + /// The constraints added to the [APISchemaObject] depend on the type of validator: + /// + /// - For `ValidateType.regex` validators, the `pattern` property of the [APISchemaObject] is set to the + /// regular expression pattern specified in the validator. + /// - For `ValidateType.comparison` validators, the `minimum`, `maximum`, `exclusiveMinimum`, and + /// `exclusiveMaximum` properties of the [APISchemaObject] are set based on the comparison values + /// specified in the validator. + /// - For `ValidateType.length` validators, the `minLength`, `maxLength`, and `maximum` properties + /// of the [APISchemaObject] are set based on the length-related values specified in the validator. + /// - For `ValidateType.present` and `ValidateType.absent` validators, no constraints are added to the + /// [APISchemaObject]. + /// - For `ValidateType.oneOf` validators, the `enumerated` property of the [APISchemaObject] is set + /// to the list of allowed values specified in the validator. + /// + /// @param context The [APIDocumentContext] being used to document the API. + /// @param object The [APISchemaObject] to which constraints should be added. + /// + /// The implementation of this method is as follows: switch (type!) { case ValidateType.regex: { @@ -485,6 +645,25 @@ class Validate { } } + /// Compiles the [Validate.oneOf] validator. + /// + /// The [Validate.oneOf] validator ensures that the value of a property is one of a set of allowed values. + /// + /// This method checks the following: + /// + /// - The `_value` property must be a `List`. + /// - The type of the property being validated must be either `String`, `int`, or `bigint`. + /// - The list of allowed values must all be assignable to the type of the property being validated. + /// - The list of allowed values must not be empty. + /// + /// If any of these conditions are not met, a [ValidateCompilationError] is thrown with a descriptive error message. + /// + /// The compiled result is the list of allowed values, which will be stored in the [ValidationContext.state] property + /// during validation. + /// + /// @param typeBeingValidated The [ManagedType] of the property being validated. + /// @param relationshipInverseType If the property is a relationship, the type of the inverse property. + /// @return The list of allowed values for the [Validate.oneOf] validator. dynamic _oneOfCompiler( ManagedType typeBeingValidated, { Type? relationshipInverseType, @@ -523,6 +702,15 @@ class Validate { return options; } + /// A list of [ValidationExpression] objects representing the various comparison + /// conditions specified for this validator. + /// + /// The [ValidationExpression] objects are created based on the values of the + /// `_equalTo`, `_lessThan`, `_lessThanEqualTo`, `_greaterThan`, and + /// `_greaterThanEqualTo` instance variables. + /// + /// This method is used by the `_comparisonCompiler` method to compile the + /// comparison validator. List get _expressions { final comparisons = []; if (_equalTo != null) { @@ -558,6 +746,23 @@ class Validate { return comparisons; } + /// Compiles the comparison validator. + /// + /// This method is responsible for creating a list of [ValidationExpression] objects + /// that represent the various comparison conditions specified for this validator. + /// + /// The method performs the following tasks: + /// + /// 1. Retrieves the list of comparison expressions from the `_expressions` getter. + /// 2. For each expression, it calls the `_parseComparisonValue` method to parse and validate + /// the comparison value based on the type of the property being validated. + /// + /// The compiled result is the list of [ValidationExpression] objects, which will be stored + /// in the [ValidationContext.state] property during validation. + /// + /// @param typeBeingValidated The [ManagedType] of the property being validated. + /// @param relationshipInverseType If the property is a relationship, the type of the inverse property. + /// @return The list of [ValidationExpression] objects representing the comparison conditions. dynamic _comparisonCompiler( ManagedType? typeBeingValidated, { Type? relationshipInverseType, @@ -573,6 +778,28 @@ class Validate { return exprs; } + /// Parses the comparison value for the [Validate.compare] validator. + /// + /// This method is responsible for validating the type of the comparison value + /// and converting it to the appropriate type if necessary. + /// + /// If the property being validated is of type [DateTime], the method attempts to + /// parse the [referenceValue] as a [DateTime] using [DateTime.parse]. If the + /// parsing fails, a [ValidateCompilationError] is thrown. + /// + /// If the property being validated is not of type [DateTime], the method checks + /// if the [referenceValue] is assignable to the type of the property being + /// validated. If the types are not compatible, a [ValidateCompilationError] is + /// thrown. + /// + /// If the [relationshipInverseType] is not null, the method checks if the + /// [referenceValue] is assignable to the primary key type of the relationship + /// being validated. + /// + /// @param referenceValue The value to be used for the comparison. + /// @param typeBeingValidated The [ManagedType] of the property being validated. + /// @param relationshipInverseType If the property is a relationship, the type of the inverse property. + /// @return The parsed comparison value as a [Comparable] object. Comparable? _parseComparisonValue( dynamic referenceValue, ManagedType? typeBeingValidated, { @@ -611,6 +838,27 @@ class Validate { return referenceValue as Comparable?; } + /// Compiles the regular expression validator. + /// + /// This method is responsible for compiling the regular expression pattern specified + /// in the `Validate.matches` validator. + /// + /// The method performs the following tasks: + /// + /// 1. Checks that the property being validated is of type `String`. If not, a `ValidateCompilationError` + /// is thrown with an appropriate error message. + /// 2. Checks that the `_value` property, which should contain the regular expression pattern, + /// is of type `String`. If not, a `ValidateCompilationError` is thrown with an appropriate + /// error message. + /// 3. Creates a `RegExp` object using the regular expression pattern specified in the `_value` + /// property, and returns it as the compiled result. + /// + /// The compiled `RegExp` object will be stored in the `ValidationContext.state` property + /// during validation. + /// + /// @param typeBeingValidated The [ManagedType] of the property being validated. + /// @param relationshipInverseType If the property is a relationship, the type of the inverse property. + /// @return The compiled `RegExp` object representing the regular expression pattern. dynamic _regexCompiler( ManagedType? typeBeingValidated, { Type? relationshipInverseType, @@ -630,6 +878,25 @@ class Validate { return RegExp(_value); } + /// Compiles the length validator. + /// + /// This method is responsible for creating a list of [ValidationExpression] objects + /// that represent the various length-based conditions specified for this validator. + /// + /// The method performs the following tasks: + /// + /// 1. Checks that the property being validated is of type `String`. If not, a + /// `ValidateCompilationError` is thrown with an appropriate error message. + /// 2. Retrieves the list of length-based expressions from the `_expressions` getter. + /// 3. Checks that all the values in the expressions are of type `int`. If not, a + /// `ValidateCompilationError` is thrown with an appropriate error message. + /// + /// The compiled result is the list of [ValidationExpression] objects, which will be + /// stored in the [ValidationContext.state] property during validation. + /// + /// @param typeBeingValidated The [ManagedType] of the property being validated. + /// @param relationshipInverseType If the property is a relationship, the type of the inverse property. + /// @return The list of [ValidationExpression] objects representing the length-based conditions. dynamic _lengthCompiler( ManagedType typeBeingValidated, { Type? relationshipInverseType, diff --git a/packages/database/lib/src/persistent_store/persistent_store.dart b/packages/database/lib/src/persistent_store/persistent_store.dart index 77cc212..64a5a2a 100644 --- a/packages/database/lib/src/persistent_store/persistent_store.dart +++ b/packages/database/lib/src/persistent_store/persistent_store.dart @@ -1,32 +1,92 @@ -import 'dart:async'; +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import 'dart:async'; import 'package:protevus_database/src/managed/context.dart'; import 'package:protevus_database/src/managed/entity.dart'; import 'package:protevus_database/src/managed/object.dart'; import 'package:protevus_database/src/query/query.dart'; import 'package:protevus_database/src/schema/schema.dart'; +/// Specifies the return type for a persistent store query. +/// +/// - [rowCount]: Indicates that the query should return the number of rows affected. +/// - [rows]: Indicates that the query should return the result set as a list of rows. enum PersistentStoreQueryReturnType { rowCount, rows } -/// An interface for implementing persistent storage. +/// Specifies the return type for a persistent store query. /// /// You rarely need to use this class directly. See [Query] for how to interact with instances of this class. /// Implementors of this class serve as the bridge between [Query]s and a specific database. abstract class PersistentStore { /// Creates a new database-specific [Query]. /// - /// Subclasses override this method to provide a concrete implementation of [Query] - /// specific to this type. Objects returned from this method must implement [Query]. They - /// should mixin [QueryMixin] to most of the behavior provided by a query. + /// This method creates a new instance of a [Query] subclass that is specific to the + /// database implementation represented by this [PersistentStore]. The returned + /// [Query] instance will be capable of interacting with the database in the appropriate + /// way. + /// + /// The [context] parameter specifies the [ManagedContext] that the [Query] will be + /// associated with. The [entity] parameter specifies the [ManagedEntity] that the + /// [Query] will operate on. Optionally, [values] can be provided which will be + /// used to initialize the [Query]. + /// + /// Subclasses must override this method to provide a concrete implementation of [Query] + /// specific to this type of [PersistentStore]. The objects returned from this method + /// must implement [Query] and should mixin [QueryMixin] to inherit the majority of + /// the behavior provided by a query. Query newQuery( ManagedContext context, ManagedEntity entity, { T? values, }); - /// Executes an arbitrary command. + /// Executes an arbitrary SQL command on the database. + /// + /// This method allows you to execute any SQL command on the database managed by + /// this [PersistentStore] instance. The [sql] parameter should contain the SQL + /// statement to be executed, and the optional [substitutionValues] parameter + /// can be used to provide values to be substituted into the SQL statement, similar + /// to how a prepared statement works. + /// + /// The return value of this method is a [Future] that completes when the SQL + /// command has finished executing. The return value of the [Future] depends on + /// the type of SQL statement being executed, but it is typically `null` for + /// non-SELECT statements, or a value representing the result of the SQL statement. + /// + /// This method is intended for advanced use cases where the higher-level query + /// APIs provided by the [Query] class are not sufficient. In general, it is + /// recommended to use the [Query] class instead of calling [execute] directly, + /// as the [Query] class provides a more type-safe and database-agnostic interface + /// for interacting with the database. Future execute(String sql, {Map? substitutionValues}); + /// Executes a database query with the provided parameters. + /// + /// This method allows you to execute a database query using a format string and a map of values. + /// + /// The `formatString` parameter is a SQL string that can contain placeholders for values, which will be + /// replaced with the values from the `values` parameter. + /// + /// The `values` parameter is a map of key-value pairs, where the keys correspond to the placeholders + /// in the `formatString`, and the values are the actual values to be substituted. + /// + /// The `timeoutInSeconds` parameter specifies the maximum time, in seconds, that the query is allowed to + /// run before being cancelled. + /// + /// The optional `returnType` parameter specifies the type of return value expected from the query. If + /// `PersistentStoreQueryReturnType.rowCount` is specified, the method will return the number of rows + /// affected by the query. If `PersistentStoreQueryReturnType.rows` is specified, the method will return + /// the result set as a list of rows. + /// + /// The return value of this method is a `Future` that completes when the query has finished executing. + /// The type of the value returned by the `Future` depends on the `returnType` parameter. Future executeQuery( String formatString, Map values, @@ -34,64 +94,337 @@ abstract class PersistentStore { PersistentStoreQueryReturnType? returnType, }); + /// Executes a database transaction. + /// + /// This method allows you to execute a sequence of database operations as a single + /// atomic transaction. If any of the operations in the transaction fail, the entire + /// transaction is rolled back, ensuring data consistency. + /// + /// The `transactionContext` parameter is the `ManagedContext` in which the transaction + /// will be executed. This context must be separate from any existing `ManagedContext` + /// instances, as transactions require their own isolated context. + /// + /// The `transactionBlock` parameter is a callback function that contains the database + /// operations to be executed as part of the transaction. This function takes the + /// `transactionContext` as its argument and returns a `Future` that represents the + /// result of the transaction. + /// + /// The return value of this method is a `Future` that completes when the transaction + /// has finished executing. The value returned by the `Future` is the same as the value + /// returned by the `transactionBlock` callback. + /// + /// Example usage: + /// ```dart + /// final result = await persistentStore.transaction( + /// transactionContext, + /// (context) async { + /// final user = await User(name: 'John Doe').insert(context); + /// final account = await Account(userId: user.id, balance: 100.0).insert(context); + /// return account; + /// }, + /// ); + /// ``` Future transaction( ManagedContext transactionContext, Future Function(ManagedContext transaction) transactionBlock, ); /// Closes the underlying database connection. + /// + /// This method is used to close the database connection managed by this + /// `PersistentStore` instance. Calling this method will ensure that all + /// resources associated with the database connection are properly released, + /// and that the connection is no longer available for use. + /// + /// The return value of this method is a `Future` that completes when the + /// database connection has been successfully closed. If there is an error + /// closing the connection, the `Future` will complete with an error. Future close(); // -- Schema Ops -- + /// Creates a list of SQL statements to create a new database table. + /// + /// This method generates the necessary SQL statements to create a new database table + /// based on the provided [SchemaTable] object. The table can be created as a + /// temporary table if the `isTemporary` parameter is set to `true`. + /// + /// The returned list of strings represents the SQL statements that should be executed + /// to create the new table. The caller of this method is responsible for executing + /// these statements to create the table in the database. + /// + /// Parameters: + /// - `table`: The [SchemaTable] object that defines the structure of the new table. + /// - `isTemporary`: A boolean indicating whether the table should be created as a + /// temporary table. Temporary tables are only visible within the current session + /// and are automatically dropped when the session ends. Defaults to `false`. + /// + /// Returns: + /// A list of strings, where each string represents a SQL statement that should be + /// executed to create the new table. List createTable(SchemaTable table, {bool isTemporary = false}); + /// Generates a list of SQL statements to rename a database table. + /// + /// This method generates the necessary SQL statements to rename an existing database table. + /// + /// Parameters: + /// - `table`: The [SchemaTable] object representing the table to be renamed. + /// - `name`: The new name for the table. + /// + /// Returns: + /// A list of strings, where each string represents a SQL statement that should be + /// executed to rename the table. List renameTable(SchemaTable table, String name); + /// Generates a list of SQL statements to delete a database table. + /// + /// This method generates the necessary SQL statements to delete an existing database table. + /// + /// Parameters: + /// - `table`: The [SchemaTable] object representing the table to be deleted. + /// + /// Returns: + /// A list of strings, where each string represents a SQL statement that should be + /// executed to delete the table. List deleteTable(SchemaTable table); + /// Generates a list of SQL statements to create a unique column set for a database table. + /// + /// This method generates the necessary SQL statements to create a unique column set + /// for an existing database table. A unique column set is a set of one or more columns + /// that must have unique values for each row in the table. + /// + /// Parameters: + /// - `table`: The [SchemaTable] object representing the table for which the unique column + /// set should be created. + /// + /// Returns: + /// A list of strings, where each string represents a SQL statement that should be + /// executed to create the unique column set. List addTableUniqueColumnSet(SchemaTable table); + /// Generates a list of SQL statements to delete a unique column set for a database table. + /// + /// This method generates the necessary SQL statements to delete an existing unique column set + /// for a database table. A unique column set is a set of one or more columns + /// that must have unique values for each row in the table. + /// + /// Parameters: + /// - `table`: The [SchemaTable] object representing the table for which the unique column + /// set should be deleted. + /// + /// Returns: + /// A list of strings, where each string represents a SQL statement that should be + /// executed to delete the unique column set. List deleteTableUniqueColumnSet(SchemaTable table); + /// Generates a list of SQL statements to add a new column to a database table. + /// + /// This method generates the necessary SQL statements to add a new column to an existing + /// database table. The new column is defined by the provided [SchemaColumn] object. + /// + /// Parameters: + /// - `table`: The [SchemaTable] object representing the table to which the new column should be added. + /// - `column`: The [SchemaColumn] object that defines the new column to be added. + /// - `unencodedInitialValue`: An optional string that specifies an initial value for the new column. + /// This value will be used as the default value for the column unless the column has a specific + /// default value defined in the [SchemaColumn] object. + /// + /// Returns: + /// A list of strings, where each string represents a SQL statement that should be executed + /// to add the new column to the table. List addColumn( SchemaTable table, SchemaColumn column, { String? unencodedInitialValue, }); + /// Generates a list of SQL statements to delete a column from a database table. + /// + /// This method generates the necessary SQL statements to delete an existing column from + /// a database table. The column to be deleted is specified by the provided [SchemaTable] + /// and [SchemaColumn] objects. + /// + /// Parameters: + /// - `table`: The [SchemaTable] object representing the table from which the column + /// should be deleted. + /// - `column`: The [SchemaColumn] object representing the column to be deleted. + /// + /// Returns: + /// A list of strings, where each string represents a SQL statement that should be + /// executed to delete the specified column from the table. List deleteColumn(SchemaTable table, SchemaColumn column); + /// Generates a list of SQL statements to rename a column in a database table. + /// + /// This method generates the necessary SQL statements to rename an existing column + /// in a database table. + /// + /// Parameters: + /// - `table`: The [SchemaTable] object representing the table containing the column + /// to be renamed. + /// - `column`: The [SchemaColumn] object representing the column to be renamed. + /// - `name`: The new name for the column. + /// + /// Returns: + /// A list of strings, where each string represents a SQL statement that should be + /// executed to rename the column. List renameColumn( SchemaTable table, SchemaColumn column, String name, ); + /// Generates a list of SQL statements to alter the nullability of a column in a database table. + /// + /// This method generates the necessary SQL statements to change the nullability of an existing + /// column in a database table. The new nullability setting is specified by the `nullable` parameter + /// of the provided [SchemaColumn] object. + /// + /// Parameters: + /// - `table`: The [SchemaTable] object representing the table containing the column to be altered. + /// - `column`: The [SchemaColumn] object representing the column to be altered. + /// - `unencodedInitialValue`: An optional string that specifies an initial value for the column + /// if it is being changed from nullable to non-nullable. This value will be used to populate + /// any existing rows that have a null value in the column. + /// + /// Returns: + /// A list of strings, where each string represents a SQL statement that should be executed + /// to alter the nullability of the column. List alterColumnNullability( SchemaTable table, SchemaColumn column, String? unencodedInitialValue, ); + /// Generates a list of SQL statements to alter the uniqueness of a column in a database table. + /// + /// This method generates the necessary SQL statements to change the uniqueness of an existing + /// column in a database table. The new uniqueness setting is specified by the `unique` property + /// of the provided [SchemaColumn] object. + /// + /// Parameters: + /// - `table`: The [SchemaTable] object representing the table containing the column to be altered. + /// - `column`: The [SchemaColumn] object representing the column to be altered. + /// + /// Returns: + /// A list of strings, where each string represents a SQL statement that should be executed + /// to alter the uniqueness of the column. List alterColumnUniqueness(SchemaTable table, SchemaColumn column); + /// Generates a list of SQL statements to alter the default value of a column in a database table. + /// + /// This method generates the necessary SQL statements to change the default value of an existing + /// column in a database table. The new default value is specified by the `defaultValue` property + /// of the provided [SchemaColumn] object. + /// + /// Parameters: + /// - `table`: The [SchemaTable] object representing the table containing the column to be altered. + /// - `column`: The [SchemaColumn] object representing the column to be altered. + /// + /// Returns: + /// A list of strings, where each string represents a SQL statement that should be executed + /// to alter the default value of the column. List alterColumnDefaultValue(SchemaTable table, SchemaColumn column); + /// Generates a list of SQL statements to alter the delete rule of a column in a database table. + /// + /// This method generates the necessary SQL statements to change the delete rule of an existing + /// column in a database table. The delete rule determines what happens to the data in the + /// column when a row is deleted from the table. + /// + /// Parameters: + /// - `table`: The [SchemaTable] object representing the table containing the column to be altered. + /// - `column`: The [SchemaColumn] object representing the column to be altered. + /// + /// Returns: + /// A list of strings, where each string represents a SQL statement that should be executed + /// to alter the delete rule of the column. List alterColumnDeleteRule(SchemaTable table, SchemaColumn column); + /// Generates a list of SQL statements to add a new index to a column in a database table. + /// + /// This method generates the necessary SQL statements to add a new index to an existing + /// column in a database table. The index is defined by the provided [SchemaTable] and + /// [SchemaColumn] objects. + /// + /// Parameters: + /// - `table`: The [SchemaTable] object representing the table to which the new index + /// should be added. + /// - `column`: The [SchemaColumn] object representing the column on which the new + /// index should be created. + /// + /// Returns: + /// A list of strings, where each string represents a SQL statement that should be + /// executed to add the new index to the table. List addIndexToColumn(SchemaTable table, SchemaColumn column); + /// Generates a list of SQL statements to rename an index on a column in a database table. + /// + /// This method generates the necessary SQL statements to rename an existing index on a + /// column in a database table. + /// + /// Parameters: + /// - `table`: The [SchemaTable] object representing the table containing the index + /// to be renamed. + /// - `column`: The [SchemaColumn] object representing the column on which the index + /// is defined. + /// - `newIndexName`: The new name for the index. + /// + /// Returns: + /// A list of strings, where each string represents a SQL statement that should be + /// executed to rename the index. List renameIndex( SchemaTable table, SchemaColumn column, String newIndexName, ); + /// Generates a list of SQL statements to delete an index from a column in a database table. + /// + /// This method generates the necessary SQL statements to delete an existing index + /// from a column in a database table. + /// + /// Parameters: + /// - `table`: The [SchemaTable] object representing the table from which the index + /// should be deleted. + /// - `column`: The [SchemaColumn] object representing the column on which the index + /// is defined. + /// + /// Returns: + /// A list of strings, where each string represents a SQL statement that should be + /// executed to delete the index from the table. List deleteIndexFromColumn(SchemaTable table, SchemaColumn column); + /// Returns the current version of the database schema. + /// + /// This property returns the current version of the database schema managed by the + /// `PersistentStore` instance. The schema version is typically used to track the + /// state of the database and ensure that migrations are applied correctly when the + /// application is upgraded. + /// + /// The returned value is a `Future` that resolves to the current schema version. + /// This method should be implemented by the concrete `PersistentStore` subclass to + /// provide the appropriate implementation for the underlying database system. Future get schemaVersion; + /// Upgrades the database schema to a new version. + /// + /// This method applies a series of database migrations to upgrade the schema from the + /// specified `fromSchema` version to a new version. + /// + /// Parameters: + /// - `fromSchema`: The current schema version of the database. + /// - `withMigrations`: A list of [Migration] instances that should be applied to upgrade + /// the schema to the new version. + /// - `temporary`: If `true`, the schema upgrade will be performed on a temporary table + /// instead of the main database table. This can be useful for testing or other + /// advanced use cases. + /// + /// Returns: + /// A `Future` that completes with the new schema version after the migrations + /// have been successfully applied. Future upgrade( Schema fromSchema, List withMigrations, { diff --git a/packages/database/lib/src/query/error.dart b/packages/database/lib/src/query/error.dart index 08ba948..619eddd 100644 --- a/packages/database/lib/src/query/error.dart +++ b/packages/database/lib/src/query/error.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_database/src/persistent_store/persistent_store.dart'; import 'package:protevus_database/src/query/query.dart'; import 'package:protevus_http/http.dart'; @@ -6,6 +15,12 @@ import 'package:protevus_http/http.dart'; /// /// A suggested HTTP status code based on the type of exception will always be available. class QueryException implements HandlerException { + /// Creates a new [QueryException] instance. + /// + /// The [event] parameter represents the type of query exception that occurred. + /// The [message] parameter is an optional error message describing the exception. + /// The [underlyingException] parameter is the underlying exception that caused the query failure. + /// The [offendingItems] parameter is a list of strings representing the items that caused the query to fail. QueryException( this.event, { this.message, @@ -13,35 +28,75 @@ class QueryException implements HandlerException { this.offendingItems, }); + /// Creates a new [QueryException] instance of type [QueryExceptionEvent.input]. + /// + /// The [message] parameter is an optional error message describing the exception. + /// The [offendingItems] parameter is a list of strings representing the items that caused the query to fail. + /// The [underlyingException] parameter is the underlying exception that caused the query failure. QueryException.input( this.message, this.offendingItems, { this.underlyingException, }) : event = QueryExceptionEvent.input; + + /// Creates a new [QueryException] instance of type [QueryExceptionEvent.transport]. + /// + /// The [message] parameter is an optional error message describing the exception. + /// The [underlyingException] parameter is the underlying exception that caused the query failure. QueryException.transport(this.message, {this.underlyingException}) : event = QueryExceptionEvent.transport, offendingItems = null; + + /// Creates a new [QueryException] instance of type [QueryExceptionEvent.conflict]. + /// + /// The [message] parameter is an optional error message describing the exception. + /// The [offendingItems] parameter is a list of strings representing the items that caused the query to fail. + /// The [underlyingException] parameter is the underlying exception that caused the query failure. QueryException.conflict( this.message, this.offendingItems, { this.underlyingException, }) : event = QueryExceptionEvent.conflict; + /// The optional error message describing the exception. final String? message; /// The exception generated by the [PersistentStore] or other mechanism that caused [Query] to fail. + /// + /// This property holds the underlying exception that led to the query failure. It can be used to provide more detailed information about the cause of the failure. final T? underlyingException; /// The type of event that caused this exception. + /// + /// This property indicates the specific type of query exception that occurred. The possible values are: + /// + /// - `QueryExceptionEvent.input`: Indicates that the input data used in the query was invalid or caused an issue. + /// - `QueryExceptionEvent.transport`: Indicates that the underlying transport mechanism (e.g., database connection) failed. + /// - `QueryExceptionEvent.conflict`: Indicates that a unique constraint was violated in the underlying data store. final QueryExceptionEvent event; + /// The list of strings representing the items that caused the query to fail. + /// + /// This property is only available when the [QueryExceptionEvent] is of type [QueryExceptionEvent.input] or [QueryExceptionEvent.conflict]. It is `null` for other exception types. final List? offendingItems; + /// Returns a [Response] object based on the type of [QueryException] that was thrown. + /// + /// The response will have the appropriate HTTP status code based on the [QueryExceptionEvent] type, and the response body will contain an error message and, if applicable, a list of offending items that caused the query to fail. @override Response get response { return Response(_getStatus(event), null, _getBody(message, offendingItems)); } + /// Generates the response body for a [QueryException] based on the exception type and details. + /// + /// The response body will contain an "error" field with the error message, and potentially a "detail" field if there are offending items that caused the query to fail. + /// + /// If [message] is `null`, the "error" field will default to "query failed". + /// + /// If [offendingItems] is not `null` and is not empty, a "detail" field will be added to the response body, listing the offending items separated by commas. + /// + /// Returns a map representing the response body. static Map _getBody( String? message, List? offendingItems, @@ -57,6 +112,13 @@ class QueryException implements HandlerException { return body; } + /// Retrieves the appropriate HTTP status code based on the [QueryExceptionEvent] type. + /// + /// This method maps the different [QueryExceptionEvent] types to their corresponding HTTP status codes: + /// + /// - [QueryExceptionEvent.input]: Returns 400 (Bad Request) + /// - [QueryExceptionEvent.transport]: Returns 503 (Service Unavailable) + /// - [QueryExceptionEvent.conflict]: Returns 409 (Conflict) static int _getStatus(QueryExceptionEvent event) { switch (event) { case QueryExceptionEvent.input: @@ -68,11 +130,20 @@ class QueryException implements HandlerException { } } + /// Returns a string representation of the [QueryException]. + /// + /// The returned string includes the error message and the underlying exception that caused the query failure. @override String toString() => "Query failed: $message. Reason: $underlyingException"; } /// Categorizations of query failures for [QueryException]. +/// +/// This enum defines the different types of query exceptions that can occur when interacting with a [PersistentStore] or performing a [Query]. The enum values are used to indicate the specific cause of a query failure, which helps [Controller]s determine the appropriate HTTP status code to return. +/// +/// - `conflict`: Indicates that a unique constraint was violated in the underlying data store. [Controller]s interpret this exception to return a status code 409 (Conflict) by default. +/// - `transport`: Indicates that the underlying transport mechanism (e.g., database connection) failed. [Controller]s interpret this exception to return a status code 503 (Service Unavailable) by default. +/// - `input`: Indicates that the input data used in the query was invalid or caused an issue. [Controller]s interpret this exception to return a status code 400 (Bad Request) by default. enum QueryExceptionEvent { /// This event is used when the underlying [PersistentStore] reports that a unique constraint was violated. /// diff --git a/packages/database/lib/src/query/matcher_expression.dart b/packages/database/lib/src/query/matcher_expression.dart index 2650912..29b8c2d 100644 --- a/packages/database/lib/src/query/matcher_expression.dart +++ b/packages/database/lib/src/query/matcher_expression.dart @@ -1,7 +1,22 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_database/src/managed/managed.dart'; import 'package:protevus_database/src/query/query.dart'; /// Contains binary logic operations to be applied to a [QueryExpression]. +/// +/// This class represents a junction of two [QueryExpression] instances, allowing for the creation of more complex +/// expressions through the use of logical operators like `and`, `or`, and `not`. +/// +/// You do not create instances of this type directly, but instead it is returned when you invoke methods like +/// [QueryExpression.and], [QueryExpression.or], and [QueryExpression.not] on a [QueryExpression]. class QueryExpressionJunction { QueryExpressionJunction._(this.lhs); @@ -18,19 +33,47 @@ class QueryExpressionJunction { /// ..where((e) => e.name).equalTo("Bob"); /// class QueryExpression { + /// Creates a new instance of [QueryExpression] with the specified [keyPath]. + /// + /// The [keyPath] represents the property path for the expression being created. QueryExpression(this.keyPath); + /// Creates a new [QueryExpression] by adding a key to the [keyPath] of the provided [original] expression. + /// + /// This method is used to create a new [QueryExpression] by appending a new [ManagedPropertyDescription] to the + /// [keyPath] of an existing [QueryExpression]. The resulting [QueryExpression] will have the same [_expression] as + /// the [original] expression, but with an updated [keyPath] that includes the additional key. + /// + /// This method is typically used when navigating through nested properties in a data model, allowing you to + /// build up complex query expressions by adding new keys to the path. + /// + /// @param original The original [QueryExpression] to use as the base. + /// @param byAdding The [ManagedPropertyDescription] to add to the [keyPath] of the original expression. + /// + /// @return A new [QueryExpression] with the updated [keyPath]. QueryExpression.byAddingKey( QueryExpression original, ManagedPropertyDescription byAdding, ) : keyPath = KeyPath.byAddingKey(original.keyPath, byAdding), _expression = original.expression; + /// The key path associated with this query expression. + /// + /// The key path represents the property path for the expression being created. final KeyPath keyPath; - // todo: This needs to be extended to an expr tree + /// Gets or sets the predicate expression associated with this query expression. + /// + /// The predicate expression represents the logical conditions that will be applied to the query. + /// When setting the expression, you can also invert the expression by using the [not] method. PredicateExpression? get expression => _expression; + /// Sets the predicate expression associated with this query expression. + /// + /// When setting the expression, you can also invert the expression by using the [not] method. + /// If the [_invertNext] flag is set to `true`, the expression will be inverted before being + /// assigned to the [_expression] field. After the expression is set, the [_invertNext] flag + /// is reset to `false`. set expression(PredicateExpression? expr) { if (_invertNext) { _expression = expr!.inverse; @@ -40,16 +83,34 @@ class QueryExpression { } } + /// A flag that indicates whether the next expression should be inverted. + /// + /// When this flag is set to `true`, the next expression that is set using the `expression` property + /// will be inverted before being assigned. After the expression is set, the flag is reset to `false`. bool _invertNext = false; + + /// The predicate expression associated with this query expression. + /// + /// The predicate expression represents the logical conditions that will be applied to the query. + /// When setting the expression, you can also invert the expression by using the [not] method. PredicateExpression? _expression; + /// Creates a new [QueryExpressionJunction] instance with the current [QueryExpression] as the left-hand side. + /// + /// This method is used internally to create a new [QueryExpressionJunction] instance that represents the logical junction + /// between the current [QueryExpression] and another [QueryExpression]. + /// + /// The resulting [QueryExpressionJunction] instance can be used to further build up complex query expressions using + /// methods like [and], [or], and [not]. + /// + /// @return A new [QueryExpressionJunction] instance with the current [QueryExpression] as the left-hand side. QueryExpressionJunction _createJunction() => QueryExpressionJunction._(this); /// Inverts the next expression. /// /// You use this method to apply an inversion to the expression that follows. For example, - /// the following example would only return objects where the 'id' is *not* equal to '5'. + /// the following example would only return objects where the 'id' is *not* equal to '5': /// /// final query = new Query() /// ..where((e) => e.name).not.equalTo("Bob"); @@ -122,7 +183,7 @@ class QueryExpression { return _createJunction(); } - /// Adds a like expression to a query. + /// Adds a 'like' expression to a query. /// /// A query will only return objects where the selected property is like [value]. /// diff --git a/packages/database/lib/src/query/mixin.dart b/packages/database/lib/src/query/mixin.dart index 6450adb..1b6d036 100644 --- a/packages/database/lib/src/query/mixin.dart +++ b/packages/database/lib/src/query/mixin.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_database/src/managed/backing.dart'; import 'package:protevus_database/src/managed/managed.dart'; import 'package:protevus_database/src/managed/relationship_type.dart'; @@ -5,45 +14,148 @@ import 'package:protevus_database/src/query/page.dart'; import 'package:protevus_database/src/query/query.dart'; import 'package:protevus_database/src/query/sort_descriptor.dart'; +/// A mixin that provides the implementation for the [Query] interface. +/// +/// This mixin is used to add the functionality of the [Query] interface to a class +/// that represents a database query. It provides methods for setting and retrieving +/// properties of the query, such as the offset, fetch limit, timeout, and value map. +/// It also provides methods for creating and managing subqueries, sorting and paging +/// the results, and validating the input values. mixin QueryMixin implements Query { + /// The offset of the query, which determines the starting point for the results. + /// + /// The offset is used to skip a certain number of results from the beginning of the + /// query. For example, an offset of 10 would skip the first 10 results and return + /// the 11th result and all subsequent results. @override int offset = 0; + /// The maximum number of results to fetch from the database. + /// + /// When the fetch limit is set to a non-zero value, the query will only return + /// up to that many results. A fetch limit of 0 (the default) means that there + /// is no limit on the number of results that can be returned. @override int fetchLimit = 0; + /// The maximum number of seconds the query is allowed to run before it is terminated. + /// + /// The timeout is used to ensure that queries don't run indefinitely, which could + /// cause issues in a production environment. If a query takes longer than the + /// specified timeout, it will be automatically terminated and an error will be + /// returned. @override int timeoutInSeconds = 30; + /// Determines whether the query can modify all instances of the entity, regardless of + /// any filtering or sorting criteria that may have been applied. + /// + /// When this property is set to `true`, the query will be able to modify all instances + /// of the entity, even if the query has filters or sorting applied that would normally + /// limit the set of instances that would be modified. + /// + /// This property is typically used in administrative or management scenarios, where + /// the user may need to perform a global modification of all instances of an entity, + /// regardless of any specific criteria. @override bool canModifyAllInstances = false; + /// The value map associated with this query. + /// + /// The value map is a dictionary that maps property names to their corresponding values. + /// This map is used to specify the values to be inserted or updated when the query is executed. @override Map? valueMap; + /// The predicate of the query, which determines the conditions that must be met for a record to be included in the results. + /// + /// The predicate is a boolean expression that is evaluated for each record in the database. Only records for which the predicate + /// evaluates to `true` will be included in the query results. @override QueryPredicate? predicate; + /// The sort predicate of the query, which determines the order in which the results of the query are returned. + /// + /// The sort predicate is a list of `QuerySortDescriptor` objects, each of which specifies a property to sort by and the + /// direction of the sort (ascending or descending). The results of the query will be sorted according to the order + /// of the sort descriptors in the predicate. @override QuerySortPredicate? sortPredicate; + /// The page descriptor for this query, which determines the ordering and + /// bounding values for the results. + /// + /// The page descriptor is used to paginate the results of the query, allowing + /// the client to retrieve the results in smaller chunks rather than all at + /// once. It specifies the property to sort the results by, the sort order, + /// and an optional bounding value to limit the results to a specific range. QueryPage? pageDescriptor; + + /// The list of sort descriptors for this query. + /// + /// The sort descriptors specify the properties to sort the query results by + /// and the sort order (ascending or descending) for each property. final List sortDescriptors = []; + + /// A dictionary that maps ManagedRelationshipDescription objects to Query objects. + /// + /// This dictionary is used to store the subqueries that are created when the [join] method is called on the QueryMixin. + /// Each key in the dictionary represents a relationship in the database, and the corresponding value is the subquery + /// that was created to fetch the data for that relationship. final Map subQueries = {}; + /// The parent query of this query, if any. + /// + /// This property is used to keep track of the parent query when this query is a + /// subquery created by the [join] method. It is used to ensure that the subquery + /// does not create a cyclic join. QueryMixin? _parentQuery; + + /// A list of `QueryExpression` objects that represent the expressions used in the query. + /// + /// The `QueryExpression` objects define the conditions that must be met for a record to be included in the query results. + /// Each expression represents a single condition, and the list of expressions is combined using the logical `AND` operator + /// to form the final predicate for the query. List> expressions = []; + + /// The value object associated with this query. + /// + /// This property represents the entity instance that will be used as the + /// values for the query. It is used to set the values that will be inserted + /// or updated when the query is executed. InstanceType? _valueObject; + /// The list of properties to fetch for this query. + /// + /// This property is initialized to the entity's default properties if it has not + /// been explicitly set. The properties are represented as `KeyPath` objects, which + /// encapsulate the path to the property within the entity. List? _propertiesToFetch; + /// The list of properties to fetch for this query. + /// + /// This property is initialized to the entity's default properties if it has not + /// been explicitly set. The properties are represented as `KeyPath` objects, which + /// encapsulate the path to the property within the entity. List get propertiesToFetch => _propertiesToFetch ?? entity.defaultProperties! .map((k) => KeyPath(entity.properties[k])) .toList(); + /// The value object associated with this query. + /// + /// This property represents the entity instance that will be used as the + /// values for the query. It is used to set the values that will be inserted + /// or updated when the query is executed. + /// + /// If the `_valueObject` is `null`, it is initialized to a new instance of the + /// entity, and its `backing` property is set to a new `ManagedBuilderBacking` + /// object that is created from the entity and the current `backing` of the + /// `_valueObject`. + /// + /// The initialized `_valueObject` is then returned. @override InstanceType get values { if (_valueObject == null) { @@ -56,6 +168,14 @@ mixin QueryMixin return _valueObject!; } + /// Sets the value object associated with this query. + /// + /// If the [obj] parameter is `null`, the `_valueObject` property is set to `null`. + /// Otherwise, a new instance of the entity is created and its `backing` property + /// is set to a new `ManagedBuilderBacking` object that is created from the entity + /// and the `backing` of the provided `obj`. + /// + /// The initialized `_valueObject` is then assigned to the `_valueObject` property. @override set values(InstanceType? obj) { if (obj == null) { @@ -68,6 +188,16 @@ mixin QueryMixin ); } + /// Adds a where clause to the query, which filters the results based on a specified property. + /// + /// The `propertyIdentifier` parameter is a function that takes an instance of the `InstanceType` entity + /// and returns a value of type `T` that represents the property to filter on. + /// + /// If the `propertyIdentifier` function references more than one property, an `ArgumentError` will be + /// thrown. + /// + /// The returned `QueryExpression` object represents the expression that will be used to filter the results + /// of the query. You can call methods on this object to specify the conditions for the filter. @override QueryExpression where( T Function(InstanceType x) propertyIdentifier, @@ -84,6 +214,23 @@ mixin QueryMixin return expr; } + /// Joins a related object or set of objects to the current query. + /// + /// This method is used to fetch related objects or sets of objects as part of the + /// current query. The related objects or sets are specified using a function that + /// takes an instance of the current entity and returns either a single related + /// object or a set of related objects. + /// + /// The [object] parameter is a function that takes an instance of the current entity + /// and returns a related object of type `T`. The [set] parameter is a function that + /// takes an instance of the current entity and returns a set of related objects of + /// type `T`. + /// + /// The return value of this method is a new `Query` object that represents the + /// subquery for the related objects or set of objects. + /// + /// Throws a `StateError` if the same property is joined more than once, or if the + /// join would create a cyclic relationship. @override Query join({ T? Function(InstanceType x)? object, @@ -95,6 +242,25 @@ mixin QueryMixin return _createSubquery(desc); } + /// Sets the page descriptor for the query, which determines the ordering and + /// bounding values for the results. + /// + /// The page descriptor is used to paginate the results of the query, allowing + /// the client to retrieve the results in smaller chunks rather than all at + /// once. It specifies the property to sort the results by, the sort order, + /// and an optional bounding value to limit the results to a specific range. + /// + /// The [propertyIdentifier] parameter is a function that takes an instance of + /// the `InstanceType` entity and returns a value of type `T` that represents + /// the property to sort the results by. + /// + /// The [order] parameter specifies the sort order, which can be either + /// `QuerySortOrder.ascending` or `QuerySortOrder.descending`. + /// + /// The [boundingValue] parameter is an optional value that can be used to + /// limit the results to a specific range. Only results where the value of the + /// specified property is greater than or equal to the bounding value will be + /// returned. @override void pageBy( T Function(InstanceType x) propertyIdentifier, @@ -106,6 +272,16 @@ mixin QueryMixin QueryPage(order, attribute.name, boundingValue: boundingValue); } + /// Adds a sort descriptor to the query, which determines the order in which the results are returned. + /// + /// The [propertyIdentifier] parameter is a function that takes an instance of the `InstanceType` entity + /// and returns a value of type `T` that represents the property to sort the results by. + /// + /// The [order] parameter specifies the sort order, which can be either `QuerySortOrder.ascending` or + /// `QuerySortOrder.descending`. + /// + /// This method adds a `QuerySortDescriptor` to the `sortDescriptors` list of the query. The descriptor + /// specifies the name of the property to sort by and the sort order to use. @override void sortBy( T Function(InstanceType x) propertyIdentifier, @@ -116,6 +292,21 @@ mixin QueryMixin sortDescriptors.add(QuerySortDescriptor(attribute.name, order)); } + /// Sets the properties to be fetched by the query. + /// + /// This method allows you to specify the properties of the entity that should be + /// fetched by the query. The `propertyIdentifiers` parameter is a function that + /// takes an instance of the `InstanceType` entity and returns a list of properties + /// to be fetched. + /// + /// Note that you cannot select has-many or has-one relationship properties using + /// this method. Instead, you should use the `join` method to fetch related objects. + /// + /// If you attempt to select a has-many or has-one relationship property, an + /// `ArgumentError` will be thrown. + /// + /// The specified properties are represented as `KeyPath` objects, which encapsulate + /// the path to the property within the entity. @override void returningProperties( List Function(InstanceType x) propertyIdentifiers, @@ -137,6 +328,25 @@ mixin QueryMixin _propertiesToFetch = entity.identifyProperties(propertyIdentifiers); } + /// Validates the input values for the query. + /// + /// This method is used to validate the values associated with the query before + /// the query is executed. It checks the validity of the values based on the + /// specified `Validating` operation (`insert` or `update`). + /// + /// If the `valueMap` is `null`, the method will call the appropriate method + /// (`willInsert` or `willUpdate`) on the `values` object to prepare it for + /// the specified operation. It then calls the `validate` method on the `values` + /// object, passing the specified `Validating` operation as the `forEvent` + /// parameter. + /// + /// If the validation context returned by the `validate` method is not valid + /// (i.e., `ctx.isValid` is `false`), the method will throw a `ValidationException` + /// with the validation errors. + /// + /// Parameters: + /// - `op`: The `Validating` operation to perform (either `Validating.insert` + /// or `Validating.update`). void validateInput(Validating op) { if (valueMap == null) { if (op == Validating.insert) { @@ -152,6 +362,28 @@ mixin QueryMixin } } + /// Creates a subquery for the specified relationship. + /// + /// This method is used to create a subquery for a related object or set of objects + /// that are part of the current query. The subquery is created using the specified + /// [fromRelationship], which is a `ManagedRelationshipDescription` object that + /// describes the relationship between the current entity and the related entity. + /// + /// If the same property is joined more than once, a `StateError` will be thrown. + /// If the join would create a cyclic relationship, a `StateError` will also be + /// thrown, with a message that suggests joining on a different property. + /// + /// The returned `Query` object represents the subquery for the related objects + /// or set of objects. This subquery can be further customized using the methods + /// provided by the `Query` interface. + /// + /// Parameters: + /// - `fromRelationship`: The `ManagedRelationshipDescription` object that + /// describes the relationship between the current entity and the related entity. + /// + /// Returns: + /// A `Query` object that represents the subquery for the related objects or + /// set of objects. Query _createSubquery( ManagedRelationshipDescription fromRelationship, ) { diff --git a/packages/database/lib/src/query/page.dart b/packages/database/lib/src/query/page.dart index 16cba8c..f55479c 100644 --- a/packages/database/lib/src/query/page.dart +++ b/packages/database/lib/src/query/page.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_database/src/query/query.dart'; /// A description of a page of results to be applied to a [Query]. @@ -16,7 +25,7 @@ import 'package:protevus_database/src/query/query.dart'; class QueryPage { QueryPage(this.order, this.propertyName, {this.boundingValue}); - /// The order in which rows should be in before the page of values is searched for. + /// The order in which rows should be sorted before the page of values is searched for. /// /// The rows of a database table will be sorted according to this order on the column backing [propertyName] prior /// to this page being fetched. @@ -24,7 +33,7 @@ class QueryPage { /// The property of the model object to page on. /// - /// This property must have an inherent order, such as an [int] or [DateTime]. The database must be able to compare the values of this property using comparison operator '<' and '>'. + /// This property must have an inherent order, such as an [int] or [DateTime]. The database must be able to compare the values of this property using comparison operators like '<' and '>'. String propertyName; /// The point within an ordered set of result values in which rows will begin being fetched from. diff --git a/packages/database/lib/src/query/predicate.dart b/packages/database/lib/src/query/predicate.dart index 6d0a82a..e2ed0b0 100644 --- a/packages/database/lib/src/query/predicate.dart +++ b/packages/database/lib/src/query/predicate.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_database/src/persistent_store/persistent_store.dart'; import 'package:protevus_database/src/query/query.dart'; @@ -13,12 +22,12 @@ import 'package:protevus_database/src/query/query.dart'; /// /// var predicate = new QueryPredicate("x = @xValue", {"xValue" : 5}); class QueryPredicate { - /// Default constructor + /// Default constructor for [QueryPredicate]. /// /// The [format] and [parameters] of this predicate. [parameters] may be null. QueryPredicate(this.format, [this.parameters = const {}]); - /// Creates an empty predicate. + /// Creates an empty [QueryPredicate] instance. /// /// The format string is the empty string and parameters is the empty map. QueryPredicate.empty() @@ -27,9 +36,11 @@ class QueryPredicate { /// Combines [predicates] with 'AND' keyword. /// - /// The [format] of the return value is produced by joining together each [predicates] - /// [format] string with 'AND'. Each [parameters] from individual [predicates] is combined - /// into the returned [parameters]. + /// This factory method takes an [Iterable] of [QueryPredicate] instances and combines them + /// using the 'AND' keyword. The resulting [QueryPredicate] will have a [format] string that + /// is the concatenation of each individual [QueryPredicate]'s [format] string, separated by + /// the 'AND' keyword. The [parameters] map will be a combination of all the individual + /// [QueryPredicate]'s [parameters] maps. /// /// If there are duplicate parameter names in [predicates], they will be disambiguated by suffixing /// the parameter name in both [format] and [parameters] with a unique integer. @@ -37,8 +48,18 @@ class QueryPredicate { /// If [predicates] is null or empty, an empty predicate is returned. If [predicates] contains only /// one predicate, that predicate is returned. factory QueryPredicate.and(Iterable predicates) { + /// Filters the provided [predicates] to only include those with a non-empty [QueryPredicate.format]. + /// + /// This method creates a new list containing only the [QueryPredicate] instances from the provided [Iterable] + /// that have a non-empty [QueryPredicate.format] string. + /// + /// @param predicates The [Iterable] of [QueryPredicate] instances to filter. + /// @return A new [List] containing the [QueryPredicate] instances from [predicates] that have a non-empty [QueryPredicate.format]. final predicateList = predicates.where((p) => p.format.isNotEmpty).toList(); + /// If the provided [predicateList] is empty, this method returns an empty [QueryPredicate]. + /// + /// If the [predicateList] contains only a single predicate, this method returns that single predicate. if (predicateList.isEmpty) { return QueryPredicate.empty(); } @@ -47,10 +68,49 @@ class QueryPredicate { return predicateList.first; } - // If we have duplicate keys anywhere, we need to disambiguate them. + /// If there are duplicate parameter names in [predicates], this variable is used to + /// disambiguate them by suffixing the parameter name in both [format] and [parameters] + /// with a unique integer. int dupeCounter = 0; + + /// Stores the format strings for each predicate in the `predicateList`. + /// + /// This list is used to build the final `format` string for the combined `QueryPredicate`. final allFormatStrings = []; + + /// A map that stores the values to replace in the [format] string of a [QueryPredicate] at execution time. + /// + /// The keys of this map will be searched for in the [format] string of the [QueryPredicate] and replaced with + /// their corresponding values. This allows the [QueryPredicate] to be parameterized, rather than having + /// dynamic values directly embedded in the [format] string. final valueMap = {}; + + /// Combines the provided [QueryPredicate] instances using the 'AND' keyword. + /// + /// This method takes an [Iterable] of [QueryPredicate] instances and combines them + /// using the 'AND' keyword. The resulting [QueryPredicate] will have a [format] string that + /// is the concatenation of each individual [QueryPredicate]'s [format] string, separated by + /// the 'AND' keyword. The [parameters] map will be a combination of all the individual + /// [QueryPredicate]'s [parameters] maps. + /// + /// If there are duplicate parameter names in [predicates], they will be disambiguated by suffixing + /// the parameter name in both [format] and [parameters] with a unique integer. + /// + /// If [predicates] is null or empty, an empty predicate is returned. If [predicates] contains only + /// one predicate, that predicate is returned. + /// + /// The code performs the following steps: + /// 1. Filters the provided [predicates] to only include those with a non-empty [QueryPredicate.format]. + /// 2. If the filtered list is empty, returns an empty [QueryPredicate]. + /// 3. If the filtered list contains only one predicate, returns that predicate. + /// 4. Initializes a `dupeCounter` variable to keep track of duplicate parameter names. + /// 5. Iterates through the filtered list of [QueryPredicate] instances: + /// - If there are any duplicate parameter names, it replaces them in the `format` string and + /// the `parameters` map with a unique identifier. + /// - Adds the modified `format` string to the `allFormatStrings` list. + /// - Adds the `parameters` map (with any modifications) to the `valueMap`. + /// 6. Constructs the final `predicateFormat` string by joining the `allFormatStrings` with the 'AND' keyword. + /// 7. Returns a new [QueryPredicate] instance with the `predicateFormat` and the `valueMap`. for (final predicate in predicateList) { final duplicateKeys = predicate.parameters.keys .where((k) => valueMap.keys.contains(k)) @@ -80,7 +140,7 @@ class QueryPredicate { return QueryPredicate(predicateFormat, valueMap); } - /// The string format of the this predicate. + /// The string format of this predicate. /// /// This is the predicate text. Do not write dynamic values directly to the format string, instead, prefix an identifier with @ /// and add that identifier to the [parameters] map. @@ -88,12 +148,24 @@ class QueryPredicate { /// A map of values to replace in the format string at execution time. /// - /// Input values should not be in the format string, but instead provided in this map. - /// Keys of this map will be searched for in the format string and be replaced by the value in this map. + /// This map contains the parameter values that will be used to replace placeholders (prefixed with '@') in the [format] string when the [QueryPredicate] is executed. The keys of this map correspond to the parameter names in the [format] string, and the values are the actual values to be substituted. + /// + /// For example, if the [format] string is `"x = @xValue AND y > @yValue"`, the [parameters] map might look like `{"xValue": 5, "yValue": 10}`. When the [QueryPredicate] is executed, the placeholders `@xValue` and `@yValue` in the [format] string will be replaced with the corresponding values from the [parameters] map. + /// + /// Input values should not be directly embedded in the [format] string, but instead provided in this [parameters] map. This allows the [QueryPredicate] to be parameterized, rather than having dynamic values directly included in the [format] string. Map parameters; } -/// The operator in a comparison matcher. +/// The operator used in a comparison-based predicate expression. +/// +/// The available operators are: +/// +/// - `lessThan`: Less than +/// - `greaterThan`: Greater than +/// - `notEqual`: Not equal to +/// - `lessThanEqualTo`: Less than or equal to +/// - `greaterThanEqualTo`: Greater than or equal to +/// - `equalTo`: Equal to enum PredicateOperator { lessThan, greaterThan, @@ -103,17 +175,56 @@ enum PredicateOperator { equalTo } +/// A comparison-based predicate expression that represents a comparison between a value and a predicate operator. +/// +/// This class encapsulates a comparison between a `value` and a `PredicateOperator`. It provides a way to represent +/// comparison-based predicates in a query, such as "x < 5" or "y >= 10". +/// +/// The `value` property represents the value being compared, which can be of any type. +/// The `operator` property represents the comparison operator, which is defined by the `PredicateOperator` enum. +/// +/// The `inverse` getter returns a new `ComparisonExpression` with the opposite `PredicateOperator`. This allows you +/// to easily negate a comparison expression, such as changing "x < 5" to "x >= 5". +/// +/// The `inverseOperator` getter returns the opposite `PredicateOperator` for the current `operator`. This is used +/// to implement the `inverse` getter. class ComparisonExpression implements PredicateExpression { + /// Constructs a new instance of [ComparisonExpression]. + /// + /// The [value] parameter represents the value being compared, which can be of any type. + /// The [operator] parameter represents the comparison operator, which is defined by the [PredicateOperator] enum. const ComparisonExpression(this.value, this.operator); + /// The value being compared in the comparison-based predicate expression. + /// + /// This property represents the value that is being compared to the predicate operator in the [ComparisonExpression]. + /// The value can be of any type. final dynamic value; + + /// The comparison operator used in the comparison-based predicate expression. + /// + /// This property represents the comparison operator used in the [ComparisonExpression]. The operator is defined by + /// the [PredicateOperator] enum, which includes options such as "less than", "greater than", "equal to", and others. final PredicateOperator operator; + /// Returns a new [ComparisonExpression] with the opposite [PredicateOperator] to the current one. + /// + /// This getter creates a new [ComparisonExpression] instance with the same [value] as the current instance, + /// but with the [PredicateOperator] reversed. For example, if the current [operator] is [PredicateOperator.lessThan], + /// the returned [ComparisonExpression] will have an [operator] of [PredicateOperator.greaterThanEqualTo]. + /// + /// This allows you to easily negate a comparison expression, such as changing "x < 5" to "x >= 5". @override PredicateExpression get inverse { return ComparisonExpression(value, inverseOperator); } + /// Returns the opposite [PredicateOperator] for the current [operator]. + /// + /// This getter is used to implement the `inverse` getter of the [ComparisonExpression] class. + /// It returns the opposite operator for the current [operator]. For example, if the current + /// [operator] is [PredicateOperator.lessThan], this getter will return + /// [PredicateOperator.greaterThanEqualTo]. PredicateOperator get inverseOperator { switch (operator) { case PredicateOperator.lessThan: @@ -132,50 +243,181 @@ class ComparisonExpression implements PredicateExpression { } } -/// The operator in a string matcher. +/// The operator used in a string-based predicate expression. +/// +/// The available operators are: +/// +/// - `beginsWith`: The string must begin with the specified value. +/// - `contains`: The string must contain the specified value. +/// - `endsWith`: The string must end with the specified value. +/// - `equals`: The string must be exactly equal to the specified value. enum PredicateStringOperator { beginsWith, contains, endsWith, equals } +/// A predicate contains instructions for filtering rows when performing a [Query]. +/// +/// Predicates currently are the WHERE clause in a SQL statement and are used verbatim +/// by the [PersistentStore]. In general, you should use [Query.where] instead of using this class directly, as [Query.where] will +/// use the underlying [PersistentStore] to generate a [QueryPredicate] for you. +/// +/// A predicate has a format and parameters. The format is the [String] that comes after WHERE in a SQL query. The format may +/// have parameterized values, for which the corresponding value is in the [parameters] map. A parameter is prefixed with '@' in the format string. Currently, +/// the format string's parameter syntax is defined by the [PersistentStore] it is used on. An example of that format: +/// +/// var predicate = new QueryPredicate("x = @xValue", {"xValue" : 5}); abstract class PredicateExpression { + /// Returns a new instance of the [PredicateExpression] with the opposite condition. + /// + /// This getter creates and returns a new instance of the [PredicateExpression] with the opposite condition to the current one. + /// For example, if the current expression is "x < 5", the returned expression would be "x >= 5". PredicateExpression get inverse; } +/// A predicate expression that represents a range comparison. +/// +/// This class encapsulates a range comparison between a `lhs` (left-hand side) value and an `rhs` (right-hand side) value. +/// It provides a way to represent range-based predicates in a query, such as "x between 5 and 10" or "y not between 20 and 30". +/// +/// The `lhs` and `rhs` properties represent the left-hand side and right-hand side values of the range, respectively. +/// The `within` property determines whether the comparison is a "within" or "not within" range. +/// +/// The `inverse` getter returns a new `RangeExpression` with the opposite `within` value. This allows you +/// to easily negate a range expression, such as changing "x between 5 and 10" to "x not between 5 and 10". class RangeExpression implements PredicateExpression { + /// Constructs a new instance of [RangeExpression]. + /// + /// The [lhs] parameter represents the left-hand side value of the range comparison. + /// The [rhs] parameter represents the right-hand side value of the range comparison. + /// The [within] parameter determines whether the comparison is a "within" or "not within" range. + /// If [within] is `true` (the default), the range expression will match values that are within the range. + /// If [within] is `false`, the range expression will match values that are not within the range. const RangeExpression(this.lhs, this.rhs, {this.within = true}); + /// Determines whether the range comparison is a "within" or "not within" range. + /// + /// If `true` (the default), the range expression will match values that are within the range. + /// If `false`, the range expression will match values that are not within the range. final bool within; + + /// The left-hand side value of the range comparison. + /// + /// This property represents the left-hand side value of the range comparison in the [RangeExpression]. The type of this value + /// can be anything, as it is represented by the generic `dynamic` type. final dynamic lhs; + + /// The right-hand side value of the range comparison. + /// + /// This property represents the right-hand side value of the range comparison in the [RangeExpression]. The type of this value + /// can be anything, as it is represented by the generic `dynamic` type. final dynamic rhs; + /// Returns a new instance of the [RangeExpression] with the opposite `within` condition. + /// + /// This getter creates and returns a new instance of the [RangeExpression] with the opposite `within` condition to the current one. + /// For example, if the current `within` value is `true`, the returned expression would have `within` set to `false`. + /// This allows you to easily negate a range expression, such as changing "x between 5 and 10" to "x not between 5 and 10". @override PredicateExpression get inverse { return RangeExpression(lhs, rhs, within: !within); } } +/// A predicate expression that checks if a value is null or not null. +/// +/// This class encapsulates a null check predicate expression, which can be used to +/// filter data based on whether a value is null or not null. +/// +/// The [shouldBeNull] parameter determines whether the expression checks for a null +/// value (if true) or a non-null value (if false). +/// +/// The [inverse] getter returns a new [NullCheckExpression] with the opposite +/// [shouldBeNull] value. This allows you to easily negate a null check expression, +/// such as changing "x is null" to "x is not null". class NullCheckExpression implements PredicateExpression { + /// Constructs a new instance of [NullCheckExpression]. + /// + /// The [shouldBeNull] parameter determines whether the expression checks for a null + /// value (if `true`) or a non-null value (if `false`). The default value is `true`, + /// which means the expression will check for a null value. const NullCheckExpression({this.shouldBeNull = true}); + /// Determines whether the expression checks for a null + /// value (if `true`) or a non-null value (if `false`). The default value is `true`, + /// which means the expression will check for a null value. final bool shouldBeNull; + /// Returns a new instance of the [NullCheckExpression] with the opposite `shouldBeNull` condition. + /// + /// This getter creates and returns a new instance of the [NullCheckExpression] with the opposite `shouldBeNull` condition to the current one. + /// For example, if the current `shouldBeNull` value is `true`, the returned expression would have `shouldBeNull` set to `false`. + /// This allows you to easily negate a null check expression, such as changing "x is null" to "x is not null". @override PredicateExpression get inverse { return NullCheckExpression(shouldBeNull: !shouldBeNull); } } +/// A predicate expression that checks if a value is a member of a set. +/// +/// This class encapsulates a set membership predicate expression, which can be used to +/// filter data based on whether a value is a member of a set of values. +/// +/// The [values] parameter represents the set of values to check for membership. +/// The [within] parameter determines whether the expression checks for membership +/// (if `true`) or non-membership (if `false`). The default value is `true`, which +/// means the expression will check for membership. +/// +/// The [inverse] getter returns a new [SetMembershipExpression] with the opposite +/// [within] value. This allows you to easily negate a set membership expression, +/// such as changing "x is in the set" to "x is not in the set". class SetMembershipExpression implements PredicateExpression { + /// Constructs a new instance of [SetMembershipExpression]. + /// + /// The [values] parameter represents the set of values to check for membership. + /// The [within] parameter determines whether the expression checks for membership + /// (if `true`) or non-membership (if `false`). The default value is `true`, which + /// means the expression will check for membership. const SetMembershipExpression(this.values, {this.within = true}); + /// The set of values to check for membership. final List values; + + /// Determines whether the expression checks for membership + /// (if `true`) or non-membership (if `false`). The default value is `true`, + /// which means the expression will check for membership. final bool within; + /// Returns a new instance of the [SetMembershipExpression] with the opposite `within` condition. + /// + /// This getter creates and returns a new instance of the [SetMembershipExpression] with the opposite `within` condition to the current one. + /// For example, if the current `within` value is `true`, the returned expression would have `within` set to `false`. + /// This allows you to easily negate a set membership expression, such as changing "x is in the set" to "x is not in the set". @override PredicateExpression get inverse { return SetMembershipExpression(values, within: !within); } } +/// A predicate expression that represents a string-based comparison. +/// +/// This class encapsulates a string-based predicate expression, which can be used to +/// filter data based on string comparisons such as "begins with", "contains", "ends with", or "equals". +/// +/// The [value] property represents the string value to compare against. +/// The [operator] property represents the string comparison operator, which is defined by the [PredicateStringOperator] enum. +/// The [caseSensitive] property determines whether the comparison should be case-sensitive or not. +/// The [invertOperator] property determines whether the operator should be inverted (e.g., "not contains" instead of "contains"). +/// The [allowSpecialCharacters] property determines whether special characters should be allowed in the string comparison. +/// +/// The [inverse] getter returns a new [StringExpression] with the opposite [invertOperator] value. This allows you +/// to easily negate a string expression, such as changing "x contains 'abc'" to "x does not contain 'abc'". class StringExpression implements PredicateExpression { + /// Constructs a new instance of [StringExpression]. + /// + /// The [value] parameter represents the string value to compare against. + /// The [operator] parameter represents the string comparison operator, which is defined by the [PredicateStringOperator] enum. + /// The [caseSensitive] parameter determines whether the comparison should be case-sensitive or not. The default value is `true`. + /// The [invertOperator] parameter determines whether the operator should be inverted (e.g., "not contains" instead of "contains"). The default value is `false`. + /// The [allowSpecialCharacters] parameter determines whether special characters should be allowed in the string comparison. The default value is `true`. const StringExpression( this.value, this.operator, { @@ -184,12 +426,26 @@ class StringExpression implements PredicateExpression { this.allowSpecialCharacters = true, }); - final PredicateStringOperator operator; - final bool invertOperator; - final bool caseSensitive; - final bool allowSpecialCharacters; + /// The string value to compare against. final String value; + /// The string comparison operator, which is defined by the [PredicateStringOperator] enum. + final PredicateStringOperator operator; + + /// Determines whether the operator should be inverted (e.g., "not contains" instead of "contains"). The default value is `false`. + final bool invertOperator; + + /// Determines whether the comparison should be case-sensitive or not. The default value is `true`. + final bool caseSensitive; + + /// Determines whether special characters should be allowed in the string comparison. The default value is `true`. + final bool allowSpecialCharacters; + + /// Returns a new instance of the [StringExpression] with the opposite [invertOperator] condition. + /// + /// This getter creates and returns a new instance of the [StringExpression] with the opposite [invertOperator] condition to the current one. + /// For example, if the current [invertOperator] value is `false`, the returned expression would have [invertOperator] set to `true`. + /// This allows you to easily negate a string expression, such as changing "x contains 'abc'" to "x does not contain 'abc'". @override PredicateExpression get inverse { return StringExpression( diff --git a/packages/database/lib/src/query/query.dart b/packages/database/lib/src/query/query.dart index c3491cf..a3bdb75 100644 --- a/packages/database/lib/src/query/query.dart +++ b/packages/database/lib/src/query/query.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'dart:async'; import 'package:protevus_database/src/managed/managed.dart'; diff --git a/packages/database/lib/src/query/reduce.dart b/packages/database/lib/src/query/reduce.dart index 1078af5..44b5750 100644 --- a/packages/database/lib/src/query/reduce.dart +++ b/packages/database/lib/src/query/reduce.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'dart:async'; import 'package:protevus_database/src/managed/object.dart'; import 'package:protevus_database/src/query/query.dart'; diff --git a/packages/database/lib/src/query/sort_descriptor.dart b/packages/database/lib/src/query/sort_descriptor.dart index 7549498..30527c1 100644 --- a/packages/database/lib/src/query/sort_descriptor.dart +++ b/packages/database/lib/src/query/sort_descriptor.dart @@ -1,9 +1,23 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_database/src/query/query.dart'; /// The order in which a collection of objects should be sorted when returned from a database. /// /// See [Query.sortBy] and [Query.pageBy] for more details. class QuerySortDescriptor { + /// Creates a new [QuerySortDescriptor] instance with the specified [key] and [order]. + /// + /// The [key] parameter represents the name of the property to sort by, and the [order] + /// parameter specifies the order in which the values should be sorted, as defined by the + /// [QuerySortOrder] class. QuerySortDescriptor(this.key, this.order); /// The name of a property to sort by. @@ -12,5 +26,8 @@ class QuerySortDescriptor { /// The order in which values should be sorted. /// /// See [QuerySortOrder] for possible values. + /// This property specifies the order in which the values should be sorted, as defined by the + /// [QuerySortOrder] class. Possible values include [QuerySortOrder.ascending] and + /// [QuerySortOrder.descending]. QuerySortOrder order; } diff --git a/packages/database/lib/src/query/sort_predicate.dart b/packages/database/lib/src/query/sort_predicate.dart index ef40891..87f36f0 100644 --- a/packages/database/lib/src/query/sort_predicate.dart +++ b/packages/database/lib/src/query/sort_predicate.dart @@ -1,7 +1,25 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_database/src/query/query.dart'; -/// The order in which a collection of objects should be sorted when returned from a database. +/// Represents a predicate for sorting a collection of objects in a database query. +/// +/// This class encapsulates the information needed to sort a collection of objects +/// retrieved from a database, including the name of the property to sort by and +/// the order in which the values should be sorted. class QuerySortPredicate { + /// Constructs a new [QuerySortPredicate] instance. + /// + /// The [predicate] parameter specifies the name of the property to sort by. + /// The [order] parameter specifies the order in which the values should be + /// sorted, using one of the values from the [QuerySortOrder] enum. QuerySortPredicate( this.predicate, this.order, @@ -12,6 +30,6 @@ class QuerySortPredicate { /// The order in which values should be sorted. /// - /// See [QuerySortOrder] for possible values. + /// This property specifies the order in which the values should be sorted, using one of the values from the [QuerySortOrder] enum. QuerySortOrder order; } diff --git a/packages/database/lib/src/schema/migration.dart b/packages/database/lib/src/schema/migration.dart index 32c1d5c..6bd549b 100644 --- a/packages/database/lib/src/schema/migration.dart +++ b/packages/database/lib/src/schema/migration.dart @@ -1,13 +1,33 @@ -import 'dart:async'; +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import 'dart:async'; import 'package:protevus_database/src/persistent_store/persistent_store.dart'; import 'package:protevus_database/src/schema/schema.dart'; -/// Thrown when [Migration] encounters an error. +/// Thrown when a [Migration] encounters an error. +/// +/// This exception is used to indicate that an error occurred during the execution of a database migration. +/// The exception includes a [message] property that provides more information about the error that occurred. class MigrationException implements Exception { + /// Creates a new [MigrationException] with the given [message]. + /// + /// The [message] parameter is a string that describes the error that occurred. MigrationException(this.message); + + /// A message describing the error that occurred. String message; + /// Returns a string representation of the [MigrationException] object. + /// + /// The string representation includes the [message] property, which provides + /// a description of the error that occurred during the migration. @override String toString() => message; } @@ -54,6 +74,23 @@ abstract class Migration { /// to a database after this migration version is executed. Future seed(); + /// Generates the source code for a database schema upgrade migration. + /// + /// This method compares an existing [Schema] with a new [Schema] and generates + /// the source code for a migration class that can be used to upgrade a database + /// from the existing schema to the new schema. + /// + /// The generated migration class will have an `upgrade()` method that contains + /// the necessary schema changes, and empty `downgrade()` and `seed()` methods. + /// + /// Parameters: + /// - `existingSchema`: The current schema of the database. + /// - `newSchema`: The new schema that the database should be upgraded to. + /// - `version`: The version number of the migration. This is used to name the migration class. + /// - `changeList`: An optional list of strings that describe the changes being made in this migration. + /// + /// Returns: + /// The source code for the migration class as a string. static String sourceForSchemaUpgrade( Schema existingSchema, Schema newSchema, diff --git a/packages/database/lib/src/schema/schema.dart b/packages/database/lib/src/schema/schema.dart index c10df7c..d2dd86c 100644 --- a/packages/database/lib/src/schema/schema.dart +++ b/packages/database/lib/src/schema/schema.dart @@ -1,5 +1,13 @@ -import 'package:collection/collection.dart' show IterableExtension; +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import 'package:collection/collection.dart' show IterableExtension; import 'package:protevus_database/src/managed/managed.dart'; import 'package:protevus_database/src/schema/schema_table.dart'; @@ -55,6 +63,7 @@ class Schema { // Do not set this directly. Use _tables= instead. late List _tableStorage; + /// Sets the tables for this schema and updates each table's schema reference. set _tables(List tables) { _tableStorage = tables; for (final t in _tableStorage) { @@ -89,6 +98,9 @@ class Schema { table.schema = this; } + /// Replaces an existing table with a new one. + /// + /// Throws a [SchemaException] if the existing table is not found. void replaceTable(SchemaTable existingTable, SchemaTable newTable) { if (!_tableStorage.contains(existingTable)) { throw SchemaException( @@ -102,19 +114,11 @@ class Schema { existingTable.schema = null; } + /// Renames a table in the schema. + /// + /// This method is not yet implemented and will throw a [SchemaException]. void renameTable(SchemaTable table, String newName) { throw SchemaException("Renaming a table not yet implemented!"); -// -// if (tableForName(newName) != null) { -// throw new SchemaException("Table ${newName} already exist."); -// } -// -// if (!tables.contains(table)) { -// throw new SchemaException("Table ${table.name} does not exist in schema."); -// } -// -// // Rename indices and constraints -// table.name = newName; } /// Removes a table from this instance. @@ -154,7 +158,6 @@ class Schema { /// This class is used for comparing schemas for validation and migration. class SchemaDifference { /// Creates a new instance that represents the difference between [expectedSchema] and [actualSchema]. - /// SchemaDifference(this.expectedSchema, this.actualSchema) { for (final expectedTable in expectedSchema.tables) { final actualTable = actualSchema[expectedTable.name!]; @@ -197,6 +200,7 @@ class SchemaDifference { /// The differences, if any, between tables in [expectedSchema] and [actualSchema]. List get tableDifferences => _differingTables; + /// Returns a list of tables that need to be added to the actual schema. List get tablesToAdd { return _differingTables .where((diff) => diff.expectedTable == null && diff.actualTable != null) @@ -204,6 +208,7 @@ class SchemaDifference { .toList(); } + /// Returns a list of tables that need to be deleted from the actual schema. List get tablesToDelete { return _differingTables .where((diff) => diff.expectedTable != null && diff.actualTable == null) @@ -211,21 +216,28 @@ class SchemaDifference { .toList(); } + /// Returns a list of tables that need to be modified in the actual schema. List get tablesToModify { return _differingTables .where((diff) => diff.expectedTable != null && diff.actualTable != null) .toList(); } + /// Internal storage for differing tables. final List _differingTables = []; } /// Thrown when a [Schema] encounters an error. class SchemaException implements Exception { + /// Creates a new [SchemaException] with the given [message]. SchemaException(this.message); + /// The error message describing the schema exception. String message; + /// Returns a string representation of this exception. + /// + /// The returned string includes the phrase "Invalid schema." followed by the [message]. @override String toString() => "Invalid schema. $message"; } diff --git a/packages/database/lib/src/schema/schema_builder.dart b/packages/database/lib/src/schema/schema_builder.dart index fb9f915..5ff5ce5 100644 --- a/packages/database/lib/src/schema/schema_builder.dart +++ b/packages/database/lib/src/schema/schema_builder.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_database/src/persistent_store/persistent_store.dart'; import 'package:protevus_database/src/schema/schema.dart'; @@ -20,7 +29,7 @@ The logic that goes into testing that the commands generated to build a valid sc /// Generates SQL or Dart code that modifies a database schema. class SchemaBuilder { - /// Creates a builder starting from an existing schema. + /// Creates a new [SchemaBuilder] instance from an existing [Schema]. /// /// If [store] is null, this builder will emit [commands] that are Dart statements that replicate the methods invoked on this object. /// Otherwise, [commands] are SQL commands (for the database represented by [store]) that are equivalent to the method invoked on this object. @@ -31,7 +40,14 @@ class SchemaBuilder { /// Creates a builder starting from the empty schema. /// /// If [store] is null, this builder will emit [commands] that are Dart statements that replicate the methods invoked on this object. - /// Otherwise, [commands] are SQL commands (for the database represented by [store]) that are equivalent to the method invoked on this object. + /// Otherwise, [commands] are SQL commands (for the database represented by [store]) that are equivalent to the method invoked on this object. + /// + /// The [targetSchema] parameter specifies the desired schema to be built. The [SchemaDifference] between the empty schema and the [targetSchema] + /// will be used to generate the necessary commands to transform the empty schema into the [targetSchema]. + /// + /// The [isTemporary] flag determines whether the generated schema changes should create temporary tables. + /// + /// The optional [changeList] parameter is a list that will be populated with human-readable descriptions of the schema changes as they are generated. SchemaBuilder.toSchema( PersistentStore? store, Schema targetSchema, { @@ -44,7 +60,21 @@ class SchemaBuilder { changeList: changeList, ); - // Creates a builder + /// Creates a new [SchemaBuilder] instance from the given [SchemaDifference]. + /// + /// The [SchemaDifference] represents the changes that need to be made to the + /// input schema to arrive at the target schema. This constructor will generate + /// the necessary SQL or Dart code commands to apply those changes. + /// + /// If [store] is not null, the generated commands will be SQL commands for the + /// underlying database. If [store] is null, the generated commands will be + /// Dart expressions that replicate the method calls to build the schema. + /// + /// The [isTemporary] flag determines whether the generated schema changes + /// should create temporary tables. + /// + /// The optional [changeList] parameter is a list that will be populated with + /// human-readable descriptions of the schema changes as they are generated. SchemaBuilder.fromDifference( this.store, SchemaDifference difference, { @@ -60,9 +90,15 @@ class SchemaBuilder { } /// The starting schema of this builder. + /// + /// This property holds the initial schema that the [SchemaBuilder] instance will use as a starting point. As operations are performed on the + /// builder, the [schema] property will be updated to reflect the resulting schema. late Schema inputSchema; /// The resulting schema of this builder as operations are applied to it. + /// + /// This property holds the final schema that the [SchemaBuilder] instance will generate after applying all the requested operations. + /// As operations are performed on the builder, the [schema] property will be updated to reflect the resulting schema. late Schema schema; /// The persistent store to validate and construct operations. @@ -72,6 +108,10 @@ class SchemaBuilder { PersistentStore? store; /// Whether or not this builder should create temporary tables. + /// + /// When this flag is set to `true`, the schema commands generated by this builder will create temporary tables + /// instead of permanent tables. This can be useful for testing or other scenarios where the schema changes are + /// not intended to be persisted. bool isTemporary; /// A list of commands generated by operations performed on this builder. @@ -81,6 +121,14 @@ class SchemaBuilder { List commands = []; /// Validates and adds a table to [schema]. + /// + /// This method adds the given [table] to the current [schema] and generates the necessary SQL or Dart code + /// commands to create the table. If [store] is not null, the generated commands will be SQL commands for + /// the underlying database. If [store] is null, the generated commands will be Dart expressions that + /// replicate the method calls to build the schema. + /// + /// The [isTemporary] flag, which is inherited from the [SchemaBuilder] instance, determines whether the + /// generated schema changes should create temporary tables. void createTable(SchemaTable table) { schema.addTable(table); @@ -92,6 +140,14 @@ class SchemaBuilder { } /// Validates and renames a table in [schema]. + /// + /// This method renames the table with the [currentTableName] to the [newName]. + /// If the [currentTableName] does not exist in the [schema], a [SchemaException] + /// will be thrown. + /// + /// If [store] is not null, the generated SQL commands to rename the table + /// will be added to the [commands] list. If [store] is null, a Dart expression + /// that replicates the table renaming will be added to the [commands] list. void renameTable(String currentTableName, String newName) { final table = schema.tableForName(currentTableName); if (table == null) { @@ -107,6 +163,13 @@ class SchemaBuilder { } /// Validates and deletes a table in [schema]. + /// + /// This method removes the specified [tableName] from the current [schema] and generates the necessary SQL or Dart code + /// commands to delete the table. If [store] is not null, the generated commands will be SQL commands for + /// the underlying database. If [store] is null, the generated commands will be Dart expressions that + /// replicate the method call to delete the table. + /// + /// If the specified [tableName] does not exist in the [schema], a [SchemaException] will be thrown. void deleteTable(String tableName) { final table = schema.tableForName(tableName); if (table == null) { @@ -123,6 +186,24 @@ class SchemaBuilder { } /// Alters a table in [schema]. + /// + /// This method allows you to modify the properties of an existing table in the schema. + /// It takes a [tableName] parameter to identify the table to be modified, and a + /// [modify] callback function that accepts a [SchemaTable] parameter and allows you + /// to make changes to the table. + /// + /// If the specified [tableName] does not exist in the [schema], a [SchemaException] + /// will be thrown. + /// + /// The changes made to the table through the [modify] callback function will be + /// reflected in the [schema] and the necessary SQL commands (if [store] is not null) + /// or Dart expressions (if [store] is null) will be added to the [commands] list. + /// + /// Example usage: + /// + /// database.alterTable("users", (t) { + /// t.uniqueColumnSet = ["email", "username"]; + /// }); void alterTable( String tableName, void Function(SchemaTable targetTable) modify, @@ -182,6 +263,15 @@ class SchemaBuilder { } /// Validates and adds a column to a table in [schema]. + /// + /// This method adds the given [column] to the table with the specified [tableName] in the current [schema]. + /// If the specified [tableName] does not exist in the [schema], a [SchemaException] will be thrown. + /// + /// If [store] is not null, the necessary SQL commands to add the column will be added to the [commands] list. + /// If [store] is null, a Dart expression that replicates the call to add the column will be added to the [commands] list. + /// + /// The optional [unencodedInitialValue] parameter can be used to specify an initial value for the column when it is + /// added to a table that already has rows. This is useful when adding a non-nullable column to an existing table. void addColumn( String tableName, SchemaColumn column, { @@ -209,6 +299,14 @@ class SchemaBuilder { } /// Validates and deletes a column in a table in [schema]. + /// + /// This method removes the specified [columnName] from the table with the given [tableName] in the current [schema] + /// and generates the necessary SQL or Dart code commands to delete the column. If [store] is not null, the generated + /// commands will be SQL commands for the underlying database. If [store] is null, the generated commands will be + /// Dart expressions that replicate the method call to delete the column. + /// + /// If the specified [tableName] does not exist in the [schema], a [SchemaException] will be thrown. If the specified + /// [columnName] does not exist in the table, a [SchemaException] will also be thrown. void deleteColumn(String tableName, String columnName) { final table = schema.tableForName(tableName); if (table == null) { @@ -230,6 +328,15 @@ class SchemaBuilder { } /// Validates and renames a column in a table in [schema]. + /// + /// This method renames the column with the [columnName] to the [newName] in the + /// table with the specified [tableName]. If the [tableName] does not exist in + /// the [schema], a [SchemaException] will be thrown. If the [columnName] does + /// not exist in the table, a [SchemaException] will also be thrown. + /// + /// If [store] is not null, the generated SQL commands to rename the column + /// will be added to the [commands] list. If [store] is null, a Dart expression + /// that replicates the column renaming will be added to the [commands] list. void renameColumn(String tableName, String columnName, String newName) { final table = schema.tableForName(tableName); if (table == null) { @@ -377,18 +484,44 @@ class SchemaBuilder { } } + /// Generates the necessary schema commands to apply the given [SchemaDifference]. + /// + /// This method is responsible for generating the SQL or Dart code commands + /// required to transform the input schema into the target schema represented + /// by the [SchemaDifference]. + /// + /// The generated commands are added to the [commands] list of this [SchemaBuilder] + /// instance. If [store] is not null, the commands will be SQL commands for the + /// underlying database. If [store] is null, the commands will be Dart expressions + /// that replicate the method calls to build the schema. + /// + /// The [changeList] parameter is an optional list that will be populated with + /// human-readable descriptions of the schema changes as they are generated. + /// + /// The [temporary] flag determines whether the generated schema changes should + /// create temporary tables instead of permanent tables. void _generateSchemaCommands( SchemaDifference difference, { List? changeList, bool temporary = false, }) { - // We need to remove foreign keys from the initial table add and defer - // them until after all tables in the schema have been created. - // These can occur in both columns and multi column unique. - // We'll split the creation of those tables into two different sets - // of commands and run the difference afterwards + /// This code handles the case where a table being added to the schema + /// has a foreign key constraint. To avoid issues with the foreign key + /// constraint during the initial table creation, the foreign key + /// information is extracted and deferred until after all tables have + /// been created. This is done by creating a list of `SchemaTableDifference` + /// objects, which represent the differences between the actual and expected + /// tables, including the foreign key information. These differences are + /// then processed separately after the initial table creation. final fkDifferences = []; + /// Handles the case where a table being added to the schema has a foreign key constraint. + /// + /// To avoid issues with the foreign key constraint during the initial table creation, the foreign key + /// information is extracted and deferred until after all tables have been created. This is done by + /// creating a list of `SchemaTableDifference` objects, which represent the differences between the + /// actual and expected tables, including the foreign key information. These differences are then + /// processed separately after the initial table creation. for (final t in difference.tablesToAdd) { final copy = SchemaTable.from(t!); if (copy.hasForeignKeyInUniqueSet) { @@ -402,20 +535,59 @@ class SchemaBuilder { fkDifferences.add(SchemaTableDifference(copy, t)); } + /// Generates the necessary schema commands for the foreign key constraints in the given [SchemaDifference]. + /// + /// This method is called after all tables have been created to handle the case where a table being added to the schema + /// has a foreign key constraint. The foreign key information is extracted and deferred until after the initial table + /// creation to avoid issues with the foreign key constraint during the initial table creation process. + /// + /// The [fkDifferences] list contains `SchemaTableDifference` objects, which represent the differences between the + /// actual and expected tables, including the foreign key information. These differences are processed separately + /// after the initial table creation. + /// + /// The [changeList] parameter is an optional list that will be populated with human-readable descriptions of the + /// schema changes as they are generated. for (final td in fkDifferences) { _generateTableCommands(td, changeList: changeList); } + /// Deletes the tables specified in the [difference.tablesToDelete] list. + /// + /// For each table in the [difference.tablesToDelete] list, this method: + /// 1. Adds a human-readable description of the table deletion to the [changeList] (if provided). + /// 2. Calls the [deleteTable] method to delete the table from the schema. for (final t in difference.tablesToDelete) { changeList?.add("Deleting table '${t!.name}'"); deleteTable(t!.name!); } + /// Generates the necessary schema commands for the tables specified in the given [SchemaDifference]. + /// + /// This method is responsible for generating the SQL or Dart code commands required to modify the + /// tables in the input schema according to the changes specified in the [SchemaDifference]. + /// + /// The generated commands are added to the [commands] list of the [SchemaBuilder] instance. If [store] + /// is not null, the commands will be SQL commands for the underlying database. If [store] is null, + /// the commands will be Dart expressions that replicate the method calls to build the schema. + /// + /// The [changeList] parameter is an optional list that will be populated with human-readable + /// descriptions of the schema changes as they are generated. for (final t in difference.tablesToModify) { _generateTableCommands(t, changeList: changeList); } } + /// Generates the necessary schema commands for the tables specified in the given [SchemaDifference]. + /// + /// This method is responsible for generating the SQL or Dart code commands required to modify the + /// tables in the input schema according to the changes specified in the [SchemaDifference]. + /// + /// The generated commands are added to the [commands] list of the [SchemaBuilder] instance. If [store] + /// is not null, the commands will be SQL commands for the underlying database. If [store] is null, + /// the commands will be Dart expressions that replicate the method calls to build the schema. + /// + /// The [changeList] parameter is an optional list that will be populated with human-readable + /// descriptions of the schema changes as they are generated. void _generateTableCommands( SchemaTableDifference difference, { List? changeList, @@ -478,6 +650,16 @@ class SchemaBuilder { } } + /// Generates a Dart expression that creates a new [SchemaTable] instance with the specified columns and unique column set. + /// + /// This method is used by the [SchemaBuilder] class to generate Dart code that replicates the operations performed on the builder. + /// + /// The generated Dart expression will create a new [SchemaTable] instance with the specified table name and columns. If the table + /// has a unique column set, the expression will also include the unique column set names. + /// + /// The [table] parameter is the [SchemaTable] instance for which the Dart expression should be generated. + /// + /// Returns the generated Dart expression as a [String]. static String _getNewTableExpression(SchemaTable table) { final builder = StringBuffer(); builder.write('database.createTable(SchemaTable("${table.name}", ['); @@ -493,6 +675,18 @@ class SchemaBuilder { return builder.toString(); } + /// Generates a Dart expression that creates a new [SchemaColumn] instance with the specified properties. + /// + /// This method is used by the [SchemaBuilder] class to generate Dart code that replicates the operations performed + /// on the builder. + /// + /// The generated Dart expression will create a new [SchemaColumn] instance with the specified name, type, and other + /// properties. If the column is a foreign key, the expression will include the related table name, related column + /// name, and delete rule. + /// + /// The [column] parameter is the [SchemaColumn] instance for which the Dart expression should be generated. + /// + /// Returns the generated Dart expression as a [String]. static String _getNewColumnExpression(SchemaColumn column) { final builder = StringBuffer(); if (column.relatedTableName != null) { diff --git a/packages/database/lib/src/schema/schema_column.dart b/packages/database/lib/src/schema/schema_column.dart index 61fd015..7735fef 100644 --- a/packages/database/lib/src/schema/schema_column.dart +++ b/packages/database/lib/src/schema/schema_column.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:protevus_database/src/managed/managed.dart'; import 'package:protevus_database/src/schema/schema.dart'; @@ -5,7 +14,16 @@ import 'package:protevus_database/src/schema/schema.dart'; /// /// Instances of this type contain the database-only details of a [ManagedPropertyDescription]. class SchemaColumn { - /// Creates an instance of this type from [name], [type] and other properties. + /// Creates a new instance of [SchemaColumn] with the specified properties. + /// + /// The [name] parameter is the name of the column. + /// The [type] parameter is the [ManagedPropertyType] of the column. + /// The [isIndexed] parameter specifies whether the column should be indexed. + /// The [isNullable] parameter specifies whether the column can be null. + /// The [autoincrement] parameter specifies whether the column should be auto-incremented. + /// The [isUnique] parameter specifies whether the column should be unique. + /// The [defaultValue] parameter specifies the default value of the column. + /// The [isPrimaryKey] parameter specifies whether the column should be the primary key. SchemaColumn( this.name, ManagedPropertyType type, { @@ -20,6 +38,16 @@ class SchemaColumn { } /// A convenience constructor for properties that represent foreign key relationships. + /// + /// This constructor creates a [SchemaColumn] instance with the specified properties for a foreign key relationship. + /// + /// The [name] parameter is the name of the column. + /// The [type] parameter is the [ManagedPropertyType] of the column. + /// The [isNullable] parameter specifies whether the column can be null. + /// The [isUnique] parameter specifies whether the column should be unique. + /// The [relatedTableName] parameter specifies the name of the related table. + /// The [relatedColumnName] parameter specifies the name of the related column. + /// The [rule] parameter specifies the [DeleteRule] for the foreign key constraint. SchemaColumn.relationship( this.name, ManagedPropertyType type, { @@ -34,7 +62,19 @@ class SchemaColumn { _deleteRule = deleteRuleStringForDeleteRule(rule); } - /// Creates an instance of this type to mirror [desc]. + /// Creates a new [SchemaColumn] instance that mirrors the properties of the provided [ManagedPropertyDescription]. + /// + /// This constructor is used to create a [SchemaColumn] instance that represents the same database column as the + /// provided [ManagedPropertyDescription]. The properties of the [SchemaColumn] instance are set based on the + /// properties of the [ManagedPropertyDescription]. + /// + /// If the [ManagedPropertyDescription] is a [ManagedRelationshipDescription], the [SchemaColumn] instance + /// will be set up as a foreign key column with the appropriate related table and column names, as well as the + /// delete rule. If the [ManagedPropertyDescription] is a [ManagedAttributeDescription], the [SchemaColumn] instance + /// will be set up with the appropriate type, nullability, autoincrement, uniqueness, and indexing properties, as well + /// as the default value if it exists. + /// + /// @param desc The [ManagedPropertyDescription] to mirror. SchemaColumn.fromProperty(ManagedPropertyDescription desc) { name = desc.name; @@ -57,7 +97,11 @@ class SchemaColumn { isIndexed = desc.isIndexed; } - /// Creates a copy of [otherColumn]. + /// Creates a new instance of [SchemaColumn] that is a copy of [otherColumn]. + /// + /// This constructor creates a new [SchemaColumn] instance with the same properties as the provided [otherColumn]. + /// The new instance will have the same name, type, indexing, nullability, autoincrement, uniqueness, default value, + /// primary key status, related table name, related column name, and delete rule as the [otherColumn]. SchemaColumn.from(SchemaColumn otherColumn) { name = otherColumn.name; _type = otherColumn._type; @@ -72,7 +116,7 @@ class SchemaColumn { _deleteRule = otherColumn._deleteRule; } - /// Creates an instance of this type from [map]. + /// Creates an instance of [SchemaColumn] from the provided [map]. /// /// Where [map] is typically created by [asMap]. SchemaColumn.fromMap(Map map) { @@ -89,7 +133,12 @@ class SchemaColumn { _deleteRule = map["deleteRule"] as String?; } - /// Creates an empty instance of this type. + /// Creates a new, empty instance of [SchemaColumn]. + /// + /// This constructor creates a new [SchemaColumn] instance with all properties set to their default values. + /// + /// The new instance will have no name, no type, no indexing, be nullable, not be autoincremented, not be unique, + /// have no default value, not be a primary key, have no related table or column names, and no delete rule. SchemaColumn.empty(); /// The name of this column. @@ -97,7 +146,8 @@ class SchemaColumn { /// The [SchemaTable] this column belongs to. /// - /// May be null if not assigned to a table. + /// This property indicates the [SchemaTable] that the [SchemaColumn] instance is associated with. + /// If the [SchemaColumn] is not assigned to a specific table, this property will be `null`. SchemaTable? table; /// The [String] representation of this column's type. @@ -157,19 +207,54 @@ class SchemaColumn { } /// Whether or not this column is a foreign key column. + /// + /// This property returns `true` if the [relatedTableName] and [relatedColumnName] properties are not `null`, + /// indicating that this column represents a foreign key relationship. Otherwise, it returns `false`. bool get isForeignKey { return relatedTableName != null && relatedColumnName != null; } + /// The type of this column as a string. String? _type; + + /// The delete rule for this column if it is a foreign key column. + /// + /// Undefined if not a foreign key column. String? _deleteRule; - /// The differences between two columns. + /// Compares the current [SchemaColumn] instance with the provided [column] and returns a [SchemaColumnDifference] object + /// that represents the differences between the two columns. + /// + /// This method is used to determine the differences between the expected and actual database schema when performing + /// schema validation or database migrations. The returned [SchemaColumnDifference] object contains information about + /// any differences in the properties of the two columns, such as name, type, nullability, indexing, uniqueness, + /// default value, and delete rule. + /// + /// @param column The [SchemaColumn] instance to compare with the current instance. + /// @return A [SchemaColumnDifference] object that represents the differences between the two columns. SchemaColumnDifference differenceFrom(SchemaColumn column) { return SchemaColumnDifference(this, column); } - /// Returns string representation of [ManagedPropertyType]. + /// Returns the string representation of the provided [ManagedPropertyType]. + /// + /// This method takes a [ManagedPropertyType] instance and returns the corresponding string representation of the + /// property type. The mapping between the [ManagedPropertyType] and its string representation is as follows: + /// + /// - `ManagedPropertyType.integer` -> `"integer"` + /// - `ManagedPropertyType.doublePrecision` -> `"double"` + /// - `ManagedPropertyType.bigInteger` -> `"bigInteger"` + /// - `ManagedPropertyType.boolean` -> `"boolean"` + /// - `ManagedPropertyType.datetime` -> `"datetime"` + /// - `ManagedPropertyType.string` -> `"string"` + /// - `ManagedPropertyType.list` -> `null` + /// - `ManagedPropertyType.map` -> `null` + /// - `ManagedPropertyType.document` -> `"document"` + /// + /// If the provided [ManagedPropertyType] is not recognized, this method will return `null`. + /// + /// @param type The [ManagedPropertyType] to convert to a string representation. + /// @return The string representation of the provided [ManagedPropertyType], or `null` if it is not recognized. static String? typeStringForType(ManagedPropertyType? type) { switch (type) { case ManagedPropertyType.integer: @@ -195,7 +280,24 @@ class SchemaColumn { } } - /// Returns inverse of [typeStringForType]. + /// Returns the [ManagedPropertyType] that corresponds to the provided string representation. + /// + /// This method takes a string representation of a property type and returns the corresponding + /// [ManagedPropertyType] instance. The mapping between the string representation and the + /// [ManagedPropertyType] is as follows: + /// + /// - `"integer"` -> `ManagedPropertyType.integer` + /// - `"double"` -> `ManagedPropertyType.doublePrecision` + /// - `"bigInteger"` -> `ManagedPropertyType.bigInteger` + /// - `"boolean"` -> `ManagedPropertyType.boolean` + /// - `"datetime"` -> `ManagedPropertyType.datetime` + /// - `"string"` -> `ManagedPropertyType.string` + /// - `"document"` -> `ManagedPropertyType.document` + /// + /// If the provided string representation is not recognized, this method will return `null`. + /// + /// @param type The string representation of the property type to convert to a [ManagedPropertyType]. + /// @return The [ManagedPropertyType] that corresponds to the provided string representation, or `null` if it is not recognized. static ManagedPropertyType? typeFromTypeString(String? type) { switch (type) { case "integer": @@ -217,7 +319,18 @@ class SchemaColumn { } } - /// Returns string representation of [DeleteRule]. + /// Returns a string representation of the provided [DeleteRule]. + /// + /// This method takes a [DeleteRule] value and returns the corresponding string representation. + /// The mapping between the [DeleteRule] and its string representation is as follows: + /// + /// - [DeleteRule.cascade] -> `"cascade"` + /// - [DeleteRule.nullify] -> `"nullify"` + /// - [DeleteRule.restrict] -> `"restrict"` + /// - [DeleteRule.setDefault] -> `"default"` + /// + /// @param rule The [DeleteRule] value to convert to a string representation. + /// @return The string representation of the provided [DeleteRule], or `null` if the [DeleteRule] is not recognized. static String? deleteRuleStringForDeleteRule(DeleteRule rule) { switch (rule) { case DeleteRule.cascade: @@ -231,7 +344,20 @@ class SchemaColumn { } } - /// Returns inverse of [deleteRuleStringForDeleteRule]. + /// Converts a string representation of a [DeleteRule] to the corresponding [DeleteRule] value. + /// + /// This method takes a string representation of a [DeleteRule] and returns the corresponding [DeleteRule] value. + /// The mapping between the string representation and the [DeleteRule] value is as follows: + /// + /// - `"cascade"` -> [DeleteRule.cascade] + /// - `"nullify"` -> [DeleteRule.nullify] + /// - `"restrict"` -> [DeleteRule.restrict] + /// - `"default"` -> [DeleteRule.setDefault] + /// + /// If the provided string representation is not recognized, this method will return `null`. + /// + /// @param rule The string representation of the [DeleteRule] to convert. + /// @return The [DeleteRule] value that corresponds to the provided string representation, or `null` if it is not recognized. static DeleteRule? deleteRuleForDeleteRuleString(String? rule) { switch (rule) { case "cascade": @@ -246,7 +372,25 @@ class SchemaColumn { return null; } - /// Returns portable representation of this instance. + /// Returns a map representation of the current [SchemaColumn] instance. + /// + /// The map contains the following key-value pairs: + /// + /// - "name": the name of the column + /// - "type": the string representation of the column's [ManagedPropertyType] + /// - "nullable": whether the column is nullable + /// - "autoincrement": whether the column is auto-incremented + /// - "unique": whether the column is unique + /// - "defaultValue": the default value of the column + /// - "primaryKey": whether the column is the primary key + /// - "relatedTableName": the name of the related table (for foreign key columns) + /// - "relatedColumnName": the name of the related column (for foreign key columns) + /// - "deleteRule": the delete rule for the foreign key constraint (for foreign key columns) + /// - "indexed": whether the column is indexed + /// + /// This method is used to create a portable representation of the [SchemaColumn] instance that can be easily + /// serialized and deserialized, for example, when storing schema information in a database or + /// transferring it over a network. Map asMap() { return { "name": name, @@ -263,6 +407,15 @@ class SchemaColumn { }; } + /// Returns a string representation of the SchemaColumn instance. + /// + /// The format of the string is "[name] (-> [relatedTableName].[relatedColumnName])", where: + /// + /// - [name] is the name of the column + /// - [relatedTableName] is the name of the related table, if the column is a foreign key + /// - [relatedColumnName] is the name of the related column, if the column is a foreign key + /// + /// If the column is not a foreign key, the string will only include the column name. @override String toString() => "$name (-> $relatedTableName.$relatedColumnName)"; } @@ -272,6 +425,24 @@ class SchemaColumn { /// This class is used for comparing database columns for validation and migration. class SchemaColumnDifference { /// Creates a new instance that represents the difference between [expectedColumn] and [actualColumn]. + /// + /// This constructor creates a new [SchemaColumnDifference] instance that represents the differences between the + /// provided [expectedColumn] and [actualColumn]. The constructor compares the properties of the two columns and + /// populates the [_differingProperties] list with any differences found. + /// + /// If the [actualColumn] and [expectedColumn] have different primary key, related column name, related table name, + /// type, or autoincrement behavior, a [SchemaException] is thrown with an appropriate error message. + /// + /// The following properties are compared between the [expectedColumn] and [actualColumn]: + /// - Name (case-insensitive) + /// - Indexing + /// - Uniqueness + /// - Nullability + /// - Default value + /// - Delete rule (for foreign key columns) + /// + /// @param expectedColumn The expected [SchemaColumn] instance. + /// @param actualColumn The actual [SchemaColumn] instance. SchemaColumnDifference(this.expectedColumn, this.actualColumn) { if (actualColumn != null && expectedColumn != null) { if (actualColumn!.isPrimaryKey != expectedColumn!.isPrimaryKey) { @@ -370,21 +541,28 @@ class SchemaColumnDifference { /// The expected column. /// - /// May be null if there is no column expected. + /// This property represents the expected [SchemaColumn] instance that is being compared to the [actualColumn]. + /// If there is no expected column, this property will be `null`. final SchemaColumn? expectedColumn; - /// The actual column. + /// The actual [SchemaColumn] instance being compared. /// /// May be null if there is no actual column. final SchemaColumn? actualColumn; /// Whether or not [expectedColumn] and [actualColumn] are different. + /// + /// This property returns `true` if there are any differences between the [expectedColumn] and [actualColumn], + /// as determined by the [_differingProperties] list. It also returns `true` if one of the columns is `null` + /// while the other is not. + /// + /// The [_differingProperties] list contains the specific properties that differ between the two columns. bool get hasDifferences => _differingProperties.isNotEmpty || (expectedColumn == null && actualColumn != null) || (actualColumn == null && expectedColumn != null); - /// Human-readable list of differences between [expectedColumn] and [actualColumn]. + /// Provides a human-readable list of differences between the expected and actual database columns. /// /// Empty is there are no differences. List get errorMessages { @@ -406,16 +584,84 @@ class SchemaColumnDifference { }).toList(); } + /// A list that stores the differences between expected and actual database columns. + /// + /// This list stores the specific properties that differ between the expected [SchemaColumn] and the actual [SchemaColumn] + /// being compared. Each difference is represented by a [_PropertyDifference] instance, which contains the name of the + /// property, the expected value, and the actual value. final List<_PropertyDifference> _differingProperties = []; } +/// Represents a difference between an expected and actual database column property. +/// +/// This class is used within the `SchemaColumnDifference` class to track the specific properties that differ +/// between an expected [SchemaColumn] and an actual [SchemaColumn] being compared. +/// +/// The [name] property represents the name of the property that is different, such as "name", "isIndexed", +/// "isUnique", "isNullable", "defaultValue", or "deleteRule". +/// +/// The [expectedValue] property represents the expected value of the property, as defined in the schema. +/// +/// The [actualValue] property represents the actual value of the property, as found in the database. +/// +/// The [getErrorMessage] method returns a human-readable error message that describes the difference between +/// the expected and actual values for the property, including the name of the table and column. class _PropertyDifference { + /// Represents a difference between an expected and actual database column property. + /// + /// This class is used within the `SchemaColumnDifference` class to track the specific properties that differ + /// between an expected [SchemaColumn] and an actual [SchemaColumn] being compared. + /// + /// The [name] property represents the name of the property that is different, such as "name", "isIndexed", + /// "isUnique", "isNullable", "defaultValue", or "deleteRule". + /// + /// The [expectedValue] property represents the expected value of the property, as defined in the schema. + /// + /// The [actualValue] property represents the actual value of the property, as found in the database. + /// + /// The [getErrorMessage] method returns a human-readable error message that describes the difference between + /// the expected and actual values for the property, including the name of the table and column. _PropertyDifference(this.name, this.expectedValue, this.actualValue); + /// The name of the database column. final String name; + + /// The expected value of the database column property. + /// + /// This represents the value that is expected for the database column property, + /// as defined in the schema. It is used to compare against the actual value + /// found in the database. final dynamic expectedValue; + + /// The actual value of the database column property. + /// + /// This represents the value that is actually found in the database for the + /// column property. It is used to compare against the expected value defined + /// in the schema. final dynamic actualValue; + /// Generates an error message for a column mismatch in the database schema. + /// + /// This method constructs a detailed error message when there's a discrepancy + /// between the expected and actual values for a specific column property. + /// + /// Parameters: + /// - [actualTableName]: The name of the table where the mismatch occurred. + /// - [expectedColumnName]: The name of the column with the mismatched property. + /// + /// Returns: + /// A formatted error message string that includes: + /// - The table name + /// - The column name + /// - The expected value for the property + /// - The actual value found in the migration files + /// + /// The message follows the format: + /// "Column '[expectedColumnName]' in table '[actualTableName]' expected + /// '[expectedValue]' for '[name]', but migration files yield '[actualValue]'" + /// + /// This method is typically used during schema validation to provide clear + /// and actionable error messages for database administrators or developers. String getErrorMessage(String? actualTableName, String? expectedColumnName) { return "Column '$expectedColumnName' in table '$actualTableName' expected " "'$expectedValue' for '$name', but migration files yield '$actualValue'"; diff --git a/packages/database/lib/src/schema/schema_table.dart b/packages/database/lib/src/schema/schema_table.dart index 8ce2ebd..4b9f303 100644 --- a/packages/database/lib/src/schema/schema_table.dart +++ b/packages/database/lib/src/schema/schema_table.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'package:collection/collection.dart' show IterableExtension; import 'package:protevus_database/src/managed/managed.dart'; import 'package:protevus_database/src/managed/relationship_type.dart';