Compare commits

...

2 commits

Author SHA1 Message Date
Patrick Stewart
ef243a6e8b update: updating files with detailed comments 2024-09-08 14:43:37 -07:00
Patrick Stewart
df25885fcf update: updating files with detailed comments 2024-09-08 14:43:23 -07:00
36 changed files with 3467 additions and 148 deletions

View file

@ -1,3 +1,24 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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';

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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';

View file

@ -1,6 +1,20 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<String, dynamic> 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<String, dynamic> 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<KeyPath> 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);

View file

@ -1,5 +1,13 @@
import 'dart:async';
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<MyModel>(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<PersistentStore> _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 <T>. It would seem to be a better idea to still throw the manual Rollback
/// returns `Future<void>`. 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<void> 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<T> insertObject<T extends ManagedObject>(T object) {
final query = Query<T>(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<List<T>> insertObjects<T extends ManagedObject>(
List<T> 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<User>(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<T?> fetchObjectWithID<T extends ManagedObject>(
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.

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<ManagedEntity> 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<Type, ManagedEntity> _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<String, ManagedEntity> _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);

View file

@ -1,8 +1,37 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<ManagedDataModel, int> _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);

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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.

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<String>? _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<T>? setOf<T extends ManagedObject>(Iterable<dynamic> objects) {
return runtime.setOfImplementation(objects) as ManagedSet<T>?;
}
@ -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 = <String, APISchemaObject>{};
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<dynamic> 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<dynamic> 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,

View file

@ -1,8 +1,19 @@
import 'package:protevus_http/src/serializable.dart';
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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);
}

View file

@ -1,25 +1,149 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<ManagedPropertyDescription?> 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<dynamic>? 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);

View file

@ -1,3 +1,35 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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';

View file

@ -1,13 +1,22 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<String, dynamic> 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<T> extends Serializable {
/// IMPROVEMENT: Cache of entity.properties to reduce property loading time
///
/// This code caches the entity's properties in a `Map<String, ManagedPropertyDescription?>` 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<String, ManagedPropertyDescription?> 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<String, ManagedPropertyDescription?> 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<T> 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<T> 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<String> propertyNames) {
for (final propertyName in propertyNames) {
backing.removeProperty(propertyName);
@ -133,6 +221,15 @@ abstract class ManagedObject<T> 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<T> 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<T> 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<T> 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<String, dynamic> object) {
object.forEach((key, v) {
@ -292,13 +425,45 @@ abstract class ManagedObject<T> 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;
}

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<ManagedValidator> 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<ManagedValidator> _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,6 +176,12 @@ 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.
@ -109,13 +192,33 @@ abstract class ManagedPropertyDescription {
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<T>(
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:
/// ```
/// - <name> | <type> | Flags: <flag1> <flag2> ... <flagN>
/// ```
/// where `<name>` is the name of the property, `<type>` is the type of the property, and `<flag1>`, `<flag2>`, ..., `<flagN>` are the flags
/// corresponding to the property's characteristics (e.g., `primary_key`, `transient`, `autoincrementing`, `unique`, `defaults to <value>`, `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<T>(
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:
/// ```
/// - <name> -> '<destinationEntity.name>' | Type: <relTypeString> | Inverse: <inverseKey>
/// ```
@override
String toString() {
var relTypeString = "has-one";

View file

@ -1,2 +1,18 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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 }

View file

@ -1,5 +1,13 @@
import 'dart:collection';
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<InstanceType extends ManagedObject> extends Object
with ListMixin<InstanceType> {
/// 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<InstanceType> 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<dynamic> items) {
_innerValues = List<InstanceType>.from(items);
}
/// The internal list that stores the elements of this [ManagedSet].
late final List<InstanceType> _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<InstanceType> 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;

View file

@ -1,6 +1,18 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<T>(
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<String, dynamic> 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<Type> 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;
}

View file

@ -1,7 +1,31 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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:
{

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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:

View file

@ -1,9 +1,21 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<String> {
/// 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<ValidationExpression> get _expressions {
final comparisons = <ValidationExpression>[];
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,

View file

@ -1,32 +1,92 @@
import 'dart:async';
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<T> newQuery<T extends ManagedObject>(
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<String, dynamic>? 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<dynamic> executeQuery(
String formatString,
Map<String, dynamic> 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<T>` that represents the
/// result of the transaction.
///
/// The return value of this method is a `Future<T>` 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<T> transaction<T>(
ManagedContext transactionContext,
Future<T> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<int>` 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<int> 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<Schema>` that completes with the new schema version after the migrations
/// have been successfully applied.
Future<Schema> upgrade(
Schema fromSchema,
List<Migration> withMigrations, {

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<T> 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<T> 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<String>? 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<String, String> _getBody(
String? message,
List<String>? offendingItems,
@ -57,6 +112,13 @@ class QueryException<T> 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<T> 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.
///

View file

@ -1,7 +1,22 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<T, InstanceType> {
QueryExpressionJunction._(this.lhs);
@ -18,19 +33,47 @@ class QueryExpressionJunction<T, InstanceType> {
/// ..where((e) => e.name).equalTo("Bob");
///
class QueryExpression<T, InstanceType> {
/// 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<T, InstanceType> 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<T, InstanceType> {
}
}
/// 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<T, InstanceType> _createJunction() =>
QueryExpressionJunction<T, InstanceType>._(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<Employee>()
/// ..where((e) => e.name).not.equalTo("Bob");
@ -122,7 +183,7 @@ class QueryExpression<T, InstanceType> {
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].
///

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<InstanceType extends ManagedObject>
implements Query<InstanceType> {
/// 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<String, dynamic>? 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<QuerySortDescriptor> sortDescriptors = <QuerySortDescriptor>[];
/// 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<ManagedRelationshipDescription, Query> 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<QueryExpression<dynamic, dynamic>> 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<KeyPath>? _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<KeyPath> 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<InstanceType extends ManagedObject>
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<InstanceType extends ManagedObject>
);
}
/// 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<T, InstanceType> where<T>(
T Function(InstanceType x) propertyIdentifier,
@ -84,6 +214,23 @@ mixin QueryMixin<InstanceType extends ManagedObject>
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<T>` 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<T> join<T extends ManagedObject>({
T? Function(InstanceType x)? object,
@ -95,6 +242,25 @@ mixin QueryMixin<InstanceType extends ManagedObject>
return _createSubquery<T>(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>(
T Function(InstanceType x) propertyIdentifier,
@ -106,6 +272,16 @@ mixin QueryMixin<InstanceType extends ManagedObject>
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>(
T Function(InstanceType x) propertyIdentifier,
@ -116,6 +292,21 @@ mixin QueryMixin<InstanceType extends ManagedObject>
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<dynamic> Function(InstanceType x) propertyIdentifiers,
@ -137,6 +328,25 @@ mixin QueryMixin<InstanceType extends ManagedObject>
_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<InstanceType extends ManagedObject>
}
}
/// 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<T>` 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<T>` object that represents the subquery for the related objects or
/// set of objects.
Query<T> _createSubquery<T extends ManagedObject>(
ManagedRelationshipDescription fromRelationship,
) {

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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.

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<QueryPredicate> 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 = <String, dynamic>{};
/// 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<String, dynamic> 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<dynamic> 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(

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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';

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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';

View file

@ -1,9 +1,23 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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;
}

View file

@ -1,7 +1,25 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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;
}

View file

@ -1,13 +1,33 @@
import 'dart:async';
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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,

View file

@ -1,5 +1,13 @@
import 'package:collection/collection.dart' show IterableExtension;
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<SchemaTable> _tableStorage;
/// Sets the tables for this schema and updates each table's schema reference.
set _tables(List<SchemaTable> 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<SchemaTableDifference> get tableDifferences => _differingTables;
/// Returns a list of tables that need to be added to the actual schema.
List<SchemaTable?> 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<SchemaTable?> 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<SchemaTableDifference> get tablesToModify {
return _differingTables
.where((diff) => diff.expectedTable != null && diff.actualTable != null)
.toList();
}
/// Internal storage for differing tables.
final List<SchemaTableDifference> _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";
}

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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.
@ -32,6 +41,13 @@ class SchemaBuilder {
///
/// 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.
///
/// 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<String> 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<String>? 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 = <SchemaTableDifference>[];
/// 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<String>? 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) {

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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<String, dynamic> 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<String, dynamic> 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<String> 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'";

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* 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';

View file

@ -14,7 +14,7 @@
/// - Salt generation utilities
///
/// These components are essential for secure password hashing and storage.
library hashing;
library;
export 'package:protevus_hashing/src/pbkdf2.dart';
export 'package:protevus_hashing/src/salt.dart';

View file

@ -10,36 +10,56 @@
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
/// Instances of this type derive a key from a password, salt, and hash function.
/// Implements the PBKDF2 (Password-Based Key Derivation Function 2) algorithm.
///
/// This class is used to derive a key from a password, salt, and hash function.
/// It's particularly useful for secure password storage and key generation.
///
/// https://en.wikipedia.org/wiki/PBKDF2
class PBKDF2 {
/// Creates instance capable of generating a key.
/// Creates an instance of PBKDF2 capable of generating a key.
///
/// [hashAlgorithm] defaults to [sha256].
/// [hashAlgorithm] specifies the hash function to use. Defaults to [sha256].
PBKDF2({Hash? hashAlgorithm}) {
this.hashAlgorithm = hashAlgorithm ?? sha256;
}
/// Gets the current hash algorithm used by this PBKDF2 instance.
Hash get hashAlgorithm => _hashAlgorithm;
/// Sets the hash algorithm to be used by this PBKDF2 instance.
///
/// This also updates the internal block size based on the new algorithm.
set hashAlgorithm(Hash algorithm) {
_hashAlgorithm = algorithm;
_blockSize = _hashAlgorithm.convert([1, 2, 3]).bytes.length;
}
/// The hash algorithm used for key derivation.
///
/// This is marked as 'late' because it's initialized in the constructor or
/// when the setter is called, but not at the point of declaration.
late Hash _hashAlgorithm;
/// The block size used in the PBKDF2 algorithm.
///
/// This value is determined by the output size of the hash function being used.
/// It's initialized when the hash algorithm is set, either in the constructor
/// or when the hashAlgorithm setter is called.
late int _blockSize;
/// Hashes a [password] with a given [salt].
/// Generates a key from the given password and salt.
///
/// The length of this return value will be [keyLength].
/// [password] is the password to hash.
/// [salt] is the salt to use in the hashing process.
/// [rounds] is the number of iterations to perform.
/// [keyLength] is the desired length of the output key in bytes.
///
/// See [generateAsBase64String] for generating a random salt.
/// Returns a [List<int>] representing the generated key.
///
/// See also [generateBase64Key], which base64 encodes the key returned from this method for storage.
/// Throws a [PBKDF2Exception] if the derived key would be too long.
List<int> generateKey(
String password,
String salt,
@ -79,9 +99,16 @@ class PBKDF2 {
return key.buffer.asUint8List();
}
/// Hashed a [password] with a given [salt] and base64 encodes the result.
/// Generates a base64-encoded key from the given password and salt.
///
/// This method invokes [generateKey] and base64 encodes the result.
///
/// [password] is the password to hash.
/// [salt] is the salt to use in the hashing process.
/// [rounds] is the number of iterations to perform.
/// [keyLength] is the desired length of the output key in bytes.
///
/// Returns a [String] representing the base64-encoded generated key.
String generateBase64Key(
String password,
String salt,
@ -94,22 +121,42 @@ class PBKDF2 {
}
}
/// Thrown when [PBKDF2] throws an exception.
/// Exception thrown when an error occurs during PBKDF2 key generation.
class PBKDF2Exception implements Exception {
/// Creates a new PBKDF2Exception with the given error message.
PBKDF2Exception(this.message);
/// The error message describing the exception.
String message;
/// Returns a string representation of the PBKDF2Exception.
///
/// This method overrides the default [Object.toString] method to provide
/// a more descriptive string representation of the exception. The returned
/// string includes the exception type ("PBKDF2Exception") followed by the
/// error message.
///
/// Returns a [String] in the format "PBKDF2Exception: [error message]".
@override
String toString() => "PBKDF2Exception: $message";
}
/// A helper class for XOR operations on digests during PBKDF2 key generation.
class _XORDigestSink implements Sink<Digest> {
/// Creates a new _XORDigestSink with the given input buffer and HMAC.
_XORDigestSink(ByteData inputBuffer, Hmac hmac) {
lastDigest = hmac.convert(inputBuffer.buffer.asUint8List()).bytes;
bytes = ByteData(lastDigest.length)
..buffer.asUint8List().setRange(0, lastDigest.length, lastDigest);
}
/// Generates a hash by repeatedly applying HMAC and XOR operations.
///
/// [inputBuffer] is the initial input data.
/// [hmac] is the HMAC instance to use for hashing.
/// [rounds] is the number of iterations to perform.
///
/// Returns a [Uint8List] representing the generated hash.
static Uint8List generate(ByteData inputBuffer, Hmac hmac, int rounds) {
final hashSink = _XORDigestSink(inputBuffer, hmac);
@ -124,9 +171,15 @@ class _XORDigestSink implements Sink<Digest> {
return hashSink.bytes.buffer.asUint8List();
}
/// Stores the intermediate XOR results.
late ByteData bytes;
/// Stores the last computed digest.
late List<int> lastDigest;
/// Adds a new digest to the sink by performing an XOR operation.
///
/// [digest] is the digest to add to the sink.
@override
void add(Digest digest) {
lastDigest = digest.bytes;
@ -135,6 +188,10 @@ class _XORDigestSink implements Sink<Digest> {
}
}
/// Closes the sink and performs any necessary cleanup.
///
/// This method is required by the [Sink] interface but does not perform
/// any additional actions in this implementation.
@override
void close() {}
}