remove(zero): zero package directory switching base project to angel3

This commit is contained in:
Patrick Stewart 2024-09-22 15:56:11 -07:00
parent ea26e024ce
commit 1b2983d02b
222 changed files with 37 additions and 291020 deletions

View file

@ -0,0 +1,36 @@
temperature: 0.5
maxTokens: 30000
---
<system>
You are an expert senior software engineer. You are highly proficient in HTML, CSS, JavaScript, PHP, Python, Perl, Dart, Flutter, SQL, JSON, YAML, MongoDB, MySQL, PostgreSQL, Docker, Kubernetes, Git, GitHub, Shell, Make, Melos, CI/CD, Firebase, Blockchain Development, and Technologies with over 10+ years of experience in software and blockchain development. You are an expert in Laravel, Symfony, CodeIgniter PHP frameworks, and Conduit, Angel3, Serverpod, Riverpod, Vania, Nylo Dart frameworks. You are also an expert in writing unit tests, regular expressions, and project documentation. Additionally, you excel at educating and teaching others.
</system>
{{{ input }}}
Please create a Laravel Inspired Event Package in Dart, ensuring that the functionality remains consistent, and the code follows Dart's syntax and conventions. Follow these requirements:
1. Create: Create the entire Laravel Inspired package in Dart, Don't skip anything and do not place stubs or place holders in the created code.
1. Production Grade: Please create a production grade package.
2. Properties: Ensure that properties are declared correctly in Dart, using the appropriate syntax for private variables.
3. Constructor: Translate the PHP constructor method to Dart, ensuring proper initialization of variables.
4. Methods: Create all methods, including getters and setters, ensuring they adhere to Dart conventions.
5. Instance Creation: Create an instance of the Dart class and demonstrate calling the methods to ensure functionality is retained.
6. Error Handling: Implement error handling mechanisms in Dart to handle exceptions and errors.
7. Comments: Add any comments from the Laravel code to Dart comments. Add comments to the created Dart code to explain the logic and functionality.
8. Identify: Identify and use Dart packages and 3rd-party libraries that can replace libraries used in the PHP code.
10. Documentation: Create a README.md file that explains how to use the package, including installation, usage, and examples.
11. Testing: Create unit tests for the package to ensure functionality and reliability.
12. Code Formatting: Format the Dart code according to Dart's style guide.
13. Create a Laravel Inspired package for each of the packages that are dependent on the Event package. Be sure to reference the event package you created.
In addition to the above requirements, please also:
- Produce 100% equivalent Dart code.
- Identify and use Dart packages and 3rd-party libraries that can replace libraries used in the PHP code.
- Identify and use Dart's equivalent of PHP's built-in functions.
- Keep the ported output in the same code block. If you need to continue, begin in the same code block where you left off.
- Replace PHP's $this keyword with Dart's this where necessary.
- Use Dart's @override annotation if necessary for any overridden methods.
- Replace echo with print for output in Dart.
- Pay attention to the differences in syntax between PHP and Dart.
- Ensure the Dart code is idiomatic and leverages Dart-specific features where appropriate.
- Maintain the logic and flow of the original Laravel Package.
- Explain the changes made in the Dart code compared to the PHP code.

View file

@ -1,4 +1,4 @@
name: app-template
name: app_template
description: An absolute bare-bones protevus app.
version: 1.0.0
# repository: https://github.com/my_org/my_repo

View file

@ -1,7 +0,0 @@
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/
# Avoid committing pubspec.lock for library packages; see
# https://dart.dev/guides/libraries/private-files#pubspeclock.
pubspec.lock

View file

@ -1,3 +0,0 @@
## 1.0.0
- Initial version.

View file

@ -1,10 +0,0 @@
The MIT License (MIT)
The Laravel Framework is Copyright (c) Taylor Otwell
The Fabric Framework is Copyright (c) Vieo, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1 +0,0 @@
<p align="center"><a href="https://protevus.com" target="_blank"><img src="https://git.protevus.com/protevus/branding/raw/branch/main/protevus-logo-bg.png"></a></p>

View file

@ -1,30 +0,0 @@
# This file configures the static analysis results for your project (errors,
# warnings, and lints).
#
# This enables the 'recommended' set of lints from `package:lints`.
# This set helps identify many issues that may lead to problems when running
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
# style and format.
#
# If you want a smaller set of lints you can change this to specify
# 'package:lints/core.yaml'. These are just the most critical lints
# (the recommended set includes the core lints).
# The core lints are also what is used by pub.dev for scoring packages.
include: package:lints/recommended.yaml
# Uncomment the following section to specify additional rules.
# linter:
# rules:
# - camel_case_types
# analyzer:
# exclude:
# - path/to/excluded/files/**
# For more information about the core and recommended set of lints, see
# https://dart.dev/go/core-lints
# For additional information about configuring this file, see
# https://dart.dev/guides/language/analysis-options

View file

@ -1,26 +0,0 @@
/*
* 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 exports various components of the application framework.
///
/// It includes:
/// - Application and ApplicationServer classes for managing the application lifecycle
/// - Channel for handling request/response cycles
/// - IsolateApplicationServer and IsolateSupervisor for managing isolates
/// - Options for configuring the application
/// - Starter for initializing and running the application
library;
export 'src/application.dart';
export 'src/application_server.dart';
export 'src/channel.dart';
export 'src/isolate_application_server.dart';
export 'src/isolate_supervisor.dart';
export 'src/options.dart';
export 'src/starter.dart';

View file

@ -1,379 +0,0 @@
/*
* 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 'dart:io';
import 'dart:isolate';
import 'package:protevus_application/application.dart';
import 'package:protevus_openapi/v3.dart';
import 'package:protevus_runtime/runtime.dart';
import 'package:logging/logging.dart';
export 'application_server.dart';
export 'options.dart';
export 'starter.dart';
/// The Application class is responsible for starting and managing instances of an ApplicationChannel.
///
/// An application object opens HTTP listeners that forward requests to instances of your [ApplicationChannel].
/// It is unlikely that you need to use this class directly - the `conduit serve` command creates an application object
/// on your behalf.
class Application<T extends ApplicationChannel> {
/// A list of isolates that this application supervises.
///
/// This list contains [ApplicationIsolateSupervisor] instances, each representing
/// an isolate running a separate instance of the application. These supervisors
/// are responsible for managing the lifecycle and communication with their
/// respective isolates. The list is populated when the application starts and
/// is cleared when the application stops.
List<ApplicationIsolateSupervisor> supervisors = [];
/// The [ApplicationServer] listening for HTTP requests while under test.
///
/// This property is only valid when an application is started via [startOnCurrentIsolate].
/// It represents the server instance that handles incoming HTTP requests during testing.
/// The server is initialized when the application starts on the current isolate and
/// provides access to the underlying HTTP server and application channel.
///
/// Note: This property should not be accessed before calling [startOnCurrentIsolate],
/// as it will not be initialized until then.
late ApplicationServer server;
/// The [ApplicationChannel] handling requests while under test.
///
/// This property provides access to the application channel instance when the application
/// is started using [startOnCurrentIsolate]. It allows direct interaction with the channel
/// during testing, enabling access to its properties and methods.
///
/// The returned value is cast to type [T], which should match the generic type parameter
/// of the [Application] class.
///
/// This property is only valid and accessible after calling [startOnCurrentIsolate].
/// Attempting to access it before starting the application or when using [start] instead
/// of [startOnCurrentIsolate] may result in unexpected behavior or errors.
///
/// Usage example:
/// ```dart
/// final app = Application<MyChannel>();
/// await app.startOnCurrentIsolate();
/// final myChannel = app.channel;
/// // Now you can interact with myChannel for testing purposes
/// ```
T get channel => server.channel as T;
/// The logger that this application will write messages to.
///
/// This logger is used throughout the application to record messages, errors,
/// and other important information. It is configured with the name 'protevus',
/// which will appear as the source of all log messages generated by this logger.
///
/// The Logger class is likely from a logging package, providing various methods
/// for different log levels (e.g., info, warning, error) and potentially
/// supporting different output destinations or formatting options.
///
/// Usage of this logger helps in debugging, monitoring, and maintaining the
/// application by providing a centralized way to capture and analyze runtime
/// information.
Logger logger = Logger("protevus");
/// The options used to configure this application.
///
/// This property holds an instance of [ApplicationOptions] that contains various
/// configuration settings for the application. These options can include things
/// like port numbers, database configurations, or any other application-specific
/// settings.
///
/// The options are typically set before the application is started. It's important
/// to note that modifying these options after the application has been started
/// will not have any effect on the running application.
///
/// Example usage:
/// ```dart
/// final app = Application<MyChannel>();
/// app.options.port = 8080;
/// app.options.configurationFilePath = 'config.yaml';
/// await app.start();
/// ```
///
/// Default value is an instance of [ApplicationOptions] with default settings.
ApplicationOptions options = ApplicationOptions();
/// The duration to wait for each isolate during startup before failing.
///
/// This property sets the maximum time allowed for each isolate to start up
/// during the application's initialization process. If an isolate fails to
/// complete its startup within this time frame, a [TimeoutException] is thrown.
///
/// Defaults to 30 seconds.
Duration isolateStartupTimeout = const Duration(seconds: 30);
/// Indicates whether the application is currently running.
///
/// This will return true if [start]/[startOnCurrentIsolate] have been invoked and completed; i.e. this is the synchronous version of the [Future] returned by [start]/[startOnCurrentIsolate].
///
/// This value will return to false after [stop] has completed.
bool get isRunning => _hasFinishedLaunching;
/// Indicates whether the application has finished launching.
///
/// This boolean flag is set to true once the application has successfully
/// completed its startup process, including initializing all isolates and
/// opening HTTP listeners. It is used internally to track the application's
/// running state and is consulted by the [isRunning] getter.
///
/// The value is set to false initially and when the application is stopped,
/// and set to true at the end of successful [start] or [startOnCurrentIsolate]
/// method execution.
bool _hasFinishedLaunching = false;
/// Retrieves the [ChannelRuntime] for the current application channel type.
///
/// This getter accesses the [RuntimeContext.current] map using the generic type [T]
/// (which represents the application's channel type) as the key. It then casts
/// the retrieved value to [ChannelRuntime].
///
/// The [ChannelRuntime] object contains runtime information and utilities
/// specific to the channel type, which are used in various parts of the
/// application for setup, initialization, and execution.
///
/// This getter is used internally by the Application class to access
/// channel-specific runtime information without exposing it publicly.
ChannelRuntime get _runtime => RuntimeContext.current[T] as ChannelRuntime;
/// Starts this application, allowing it to handle HTTP requests.
///
/// This method initializes the application by spawning a specified number of isolates,
/// each running an instance of the application channel. It then sets up an HTTP listener
/// to distribute incoming requests across these isolates.
///
/// The [Future] returned from this method will complete once all isolates have successfully started
/// and are available to handle requests.
///
/// If your application channel implements [ApplicationChannel.initializeApplication],
/// it will be invoked prior to any isolate being spawned.
///
/// See also [startOnCurrentIsolate] for starting an application when running automated tests.
Future start({int numberOfInstances = 1, bool consoleLogging = false}) async {
if (supervisors.isNotEmpty) {
throw StateError(
"Application error. Cannot invoke 'start' on already running Conduit application.",
);
}
if (options.address == null) {
if (options.isIpv6Only) {
options.address = InternetAddress.anyIPv6;
} else {
options.address = InternetAddress.anyIPv4;
}
}
try {
await _runtime.runGlobalInitialization(options);
for (var i = 0; i < numberOfInstances; i++) {
final supervisor = await _spawn(
this,
options,
i + 1,
logger,
isolateStartupTimeout,
logToConsole: consoleLogging,
);
supervisors.add(supervisor);
await supervisor.resume();
}
} catch (e, st) {
logger.severe("$e", this, st);
await stop().timeout(const Duration(seconds: 5));
rethrow;
}
for (final sup in supervisors) {
sup.sendPendingMessages();
}
_hasFinishedLaunching = true;
}
/// Starts the application on the current isolate without spawning additional isolates.
///
/// An application started in this way will run on the same isolate this method is invoked on.
/// Performance is limited when running the application with this method; prefer to use [start].
Future startOnCurrentIsolate() async {
if (supervisors.isNotEmpty) {
throw StateError(
"Application error. Cannot invoke 'test' on already running Conduit application.",
);
}
options.address ??= InternetAddress.loopbackIPv4;
try {
await _runtime.runGlobalInitialization(options);
server = ApplicationServer(_runtime.channelType, options, 1);
await server.start();
_hasFinishedLaunching = true;
} catch (e, st) {
logger.severe("$e", this, st);
await stop().timeout(const Duration(seconds: 5));
rethrow;
}
}
/// Stops the application from running.
///
/// This method performs the following actions:
/// 1. Sets the '_hasFinishedLaunching' flag to false.
/// 2. Stops all supervisor isolates concurrently.
/// 3. Handles potential errors during supervisor shutdown, particularly checking for 'LateError'.
/// 4. Attempts to close the server forcefully.
/// 5. Logs any errors that occur during server closure.
/// 6. Resets the '_hasFinishedLaunching' flag and clears the supervisors list.
/// 7. Removes all listeners from the logger.
///
/// If a 'LateError' is encountered during supervisor shutdown, it throws a [StateError]
/// indicating that the channel type was not properly loaded.
///
/// This method ensures a clean shutdown of all application components and should be
/// called when the application needs to be terminated.
Future stop() async {
_hasFinishedLaunching = false;
await Future.wait(supervisors.map((s) => s.stop()))
.onError((error, stackTrace) {
if (error.runtimeType.toString() == 'LateError') {
throw StateError(
'Channel type $T was not loaded in the current isolate. Check that the class was declared and public.',
);
}
throw error! as Error;
});
try {
await server.server.close(force: true);
} catch (e) {
logger.severe(e);
}
_hasFinishedLaunching = false;
supervisors = [];
logger.clearListeners();
}
/// Creates an [APIDocument] from an [ApplicationChannel].
///
/// This static method generates API documentation for a given application channel type.
/// It is primarily used by the 'conduit document' CLI command to create OpenAPI (formerly Swagger) documentation.
///
/// The method performs the following steps:
/// 1. Retrieves the runtime context for the specified channel type.
/// 2. Runs global initialization with the provided configuration.
/// 3. Creates an ApplicationServer instance.
/// 4. Prepares the channel.
/// 5. Generates the API documentation.
/// 6. Closes the channel.
/// 7. Returns the generated APIDocument.
///
/// Parameters:
/// - [type]: The Type of the ApplicationChannel subclass.
/// - [config]: The ApplicationOptions containing configuration for the application.
/// - [projectSpec]: A Map containing additional project-specific information for the documentation.
///
/// Returns:
/// A Future that resolves to an [APIDocument] containing the generated API documentation.
///
/// This method is called by the `conduit document` CLI.
static Future<APIDocument> document(
Type type,
ApplicationOptions config,
Map<String, dynamic> projectSpec,
) async {
final runtime = RuntimeContext.current[type] as ChannelRuntime;
await runtime.runGlobalInitialization(config);
final server = ApplicationServer(runtime.channelType, config, 1);
await server.channel.prepare();
final doc = await server.channel.documentAPI(projectSpec);
await server.channel.close();
return doc;
}
/// Spawns a new isolate to run an instance of the application.
///
/// This method creates a new isolate that runs an instance of the application channel.
/// It sets up the necessary communication channels and initializes the isolate with
/// the application's configuration and runtime information.
///
/// Parameters:
/// - [application]: The main Application instance.
/// - [config]: ApplicationOptions containing the configuration for this instance.
/// - [identifier]: A unique identifier for this isolate.
/// - [logger]: The Logger instance for logging.
/// - [startupTimeout]: The maximum duration allowed for the isolate to start up.
/// - [logToConsole]: Whether to enable console logging for this isolate (default: false).
///
/// Returns:
/// A Future that resolves to an [ApplicationIsolateSupervisor] managing the new isolate.
///
/// This method is crucial for scaling the application across multiple isolates,
/// allowing for better performance and resource utilization.
Future<ApplicationIsolateSupervisor> _spawn(
Application application,
ApplicationOptions config,
int identifier,
Logger logger,
Duration startupTimeout, {
bool logToConsole = false,
}) async {
final receivePort = ReceivePort();
final libraryUri = _runtime.libraryUri;
final typeName = _runtime.name;
final entryPoint = _runtime.isolateEntryPoint;
final initialMessage = ApplicationInitialServerMessage(
typeName,
libraryUri,
config,
identifier,
receivePort.sendPort,
logToConsole: logToConsole,
);
final isolate =
await Isolate.spawn(entryPoint, initialMessage, paused: true);
return ApplicationIsolateSupervisor(
application,
isolate,
receivePort,
identifier,
logger,
startupTimeout: startupTimeout,
);
}
}
/// Represents an exception that occurs during the startup process of an application.
///
/// Contains the original exception that halted startup.
class ApplicationStartupException implements Exception {
ApplicationStartupException(this.originalException);
dynamic originalException;
@override
String toString() => originalException.toString();
}

View file

@ -1,274 +0,0 @@
/*
* 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 'dart:io';
import 'package:logging/logging.dart';
import 'package:protevus_application/application.dart';
import 'package:protevus_http/http.dart';
import 'package:protevus_runtime/runtime.dart';
/// A class representing an application server in the Conduit framework.
///
/// The ApplicationServer class is responsible for managing the lifecycle of an HTTP server
/// and its associated [ApplicationChannel]. It handles server creation, starting, and stopping,
/// as well as routing incoming requests to the appropriate handlers.
///
/// Key features:
/// - Creates and manages an instance of [ApplicationChannel]
/// - Configures and starts an HTTP or HTTPS server
/// - Handles incoming requests and routes them to the appropriate controller
/// - Manages server lifecycle (start, stop, close)
/// - Provides logging capabilities
/// - Supports both IPv4 and IPv6
/// - Handles secure connections with SSL/TLS
///
/// This class is typically instantiated and managed by the Application class and should not
/// be created directly in most cases
class ApplicationServer {
/// Creates a new server instance.
///
/// You should not need to invoke this method directly.
ApplicationServer(this.channelType, this.options, this.identifier) {
channel = (RuntimeContext.current[channelType] as ChannelRuntime)
.instantiateChannel()
..server = this
..options = options;
}
/// The configuration options used to start this server's [channel].
///
/// This property holds an instance of [ApplicationOptions] which contains
/// various settings used to configure the server, such as the address to bind to,
/// the port number, SSL/TLS settings, and other application-specific options.
/// These options are passed to the [ApplicationChannel] when it is initialized.
ApplicationOptions options;
/// The underlying [HttpServer] instance used by this [ApplicationServer].
///
/// This property represents the core HTTP server that handles incoming requests.
/// It is initialized when the server starts and is used throughout the lifecycle
/// of the [ApplicationServer] to manage incoming connections and route requests
/// to the appropriate handlers.
///
/// The server can be either a standard HTTP server or an HTTPS server, depending
/// on the configuration and security context provided during initialization.
late final HttpServer server;
/// The instance of [ApplicationChannel] serving requests.
///
/// This property represents the primary request handling pipeline for the application.
/// It is instantiated when the ApplicationServer is created and is responsible for
/// processing incoming HTTP requests, routing them to appropriate controllers,
/// and generating responses.
///
/// The [ApplicationChannel] is a custom class defined by the application developer
/// that sets up the request handling logic, including middleware, controllers,
/// and other application-specific components.
late ApplicationChannel channel;
/// The cached entrypoint of [channel].
///
/// This property stores the main [Controller] that serves as the entry point for request handling.
/// It is initialized when the server starts and is used to process incoming HTTP requests.
/// The entrypoint controller typically represents the root of the request handling pipeline
/// and may delegate to other controllers or middleware as needed.
late Controller entryPoint;
/// The type of [ApplicationChannel] this server will use.
///
/// This property stores the Type of the ApplicationChannel subclass that will be
/// instantiated and used by this ApplicationServer. The ApplicationChannel
/// defines the request handling logic and routing for the application.
final Type channelType;
/// Target for sending messages to other [ApplicationChannel.messageHub]s.
///
/// This property represents an [EventSink] that can be used to send messages
/// to other [ApplicationChannel.messageHub]s across different instances of
/// the application. It is primarily used for inter-server communication in
/// distributed setups.
///
/// The [hubSink] is typically set and managed by instances of [ApplicationMessageHub].
/// Application developers should not directly modify or use this property, as it is
/// intended for internal framework use.
///
/// The sink can be null if no message hub has been configured for this server.
EventSink<dynamic>? hubSink;
/// Indicates whether this server requires an HTTPS listener.
///
/// This getter returns a boolean value that determines if the server
/// should use HTTPS instead of HTTP. It is typically set to true when
/// a security context is provided during server initialization.
///
/// Returns:
/// [bool]: true if the server requires HTTPS, false otherwise.
bool get requiresHTTPS => _requiresHTTPS;
/// Indicates whether this server instance is configured to use HTTPS.
///
/// This private variable is set to true when a security context is provided
/// during server initialization, indicating that the server should use HTTPS.
/// It is used internally to determine the server's connection type and is
/// accessed through the public getter [requiresHTTPS].
///
/// The value is false by default, assuming HTTP connection, and is only set to
/// true when HTTPS is explicitly configured.
bool _requiresHTTPS = false;
/// The unique identifier of this instance.
///
/// Each instance has its own identifier, a numeric value starting at 1, to identify it
/// among other instances.
///
/// This identifier is used to distinguish between different [ApplicationServer] instances
/// when multiple servers are running concurrently. It's particularly useful for logging
/// and debugging purposes, allowing developers to trace which server instance is handling
/// specific requests or operations.
///
/// The identifier is typically assigned automatically by the [Application] class when
/// creating new server instances, ensuring that each server has a unique number.
///
/// Example:
/// If three server instances are created, they might have identifiers 1, 2, and 3 respectively.
int identifier;
/// Returns the logger instance for this ApplicationServer.
///
/// This getter provides access to a [Logger] instance specifically configured
/// for the Conduit framework. The logger is named "conduit" and can be used
/// throughout the ApplicationServer and its associated classes for consistent
/// logging purposes.
///
/// The logger can be used to record various levels of information, warnings,
/// and errors during the server's operation, which is crucial for debugging
/// and monitoring the application's behavior.
///
/// Returns:
/// A [Logger] instance named "conduit".
Logger get logger => Logger("protevus");
/// Starts this instance, allowing it to receive HTTP requests.
///
/// This method initializes the server, preparing it to handle incoming HTTP requests.
/// It performs the following steps:
/// 1. Prepares the channel by calling [channel.prepare()].
/// 2. Sets up the entry point for request handling.
/// 3. Binds the HTTP server to the specified address and port.
/// 4. Configures HTTPS if a security context is provided.
///
/// The method supports both HTTP and HTTPS connections, determined by the presence
/// of a security context. It also handles IPv6 configuration and server sharing options.
///
/// Parameters:
/// [shareHttpServer] - A boolean indicating whether to share the HTTP server
/// across multiple instances. Defaults to false.
///
/// Returns:
/// A [Future] that completes when the server has successfully started and is
/// ready to receive requests.
///
/// Throws:
/// May throw exceptions related to network binding or security context configuration.
///
/// Note:
/// This method should not be invoked directly under normal circumstances.
/// It is typically called by the framework during the application startup process.
Future start({bool shareHttpServer = false}) async {
logger.fine("ApplicationServer($identifier).start entry");
await channel.prepare();
entryPoint = channel.entryPoint;
entryPoint.didAddToChannel();
logger.fine("ApplicationServer($identifier).start binding HTTP");
final securityContext = channel.securityContext;
if (securityContext != null) {
_requiresHTTPS = true;
server = await HttpServer.bindSecure(
options.address,
options.port,
securityContext,
requestClientCertificate: options.isUsingClientCertificate,
v6Only: options.isIpv6Only,
shared: shareHttpServer,
);
} else {
_requiresHTTPS = false;
server = await HttpServer.bind(
options.address,
options.port,
v6Only: options.isIpv6Only,
shared: shareHttpServer,
);
}
logger.fine("ApplicationServer($identifier).start bound HTTP");
return didOpen();
}
/// Closes this HTTP server and associated channel.
///
/// This method performs the following steps:
/// 1. Closes the HTTP server, forcibly terminating any ongoing connections.
/// 2. Closes the associated [ApplicationChannel].
/// 3. Closes the [hubSink] if it exists.
///
/// The method logs the progress of each step for debugging purposes.
///
/// Returns:
/// A [Future] that completes when all closing operations are finished.
///
/// Note:
/// The [hubSink] is actually closed by channel.messageHub.close, but it's
/// explicitly closed here to satisfy the Dart analyzer.
Future close() async {
logger.fine("ApplicationServer($identifier).close Closing HTTP listener");
await server.close(force: true);
logger.fine("ApplicationServer($identifier).close Closing channel");
await channel.close();
// This is actually closed by channel.messageHub.close, but this shuts up the analyzer.
hubSink?.close();
logger.fine("ApplicationServer($identifier).close Closing complete");
}
/// Invoked when this server becomes ready to receive requests.
///
/// [ApplicationChannel.willStartReceivingRequests] is invoked after this opening has completed.
Future didOpen() async {
server.serverHeader = "conduit/$identifier";
logger.fine("ApplicationServer($identifier).didOpen start listening");
server.map((baseReq) => Request(baseReq)).listen(entryPoint.receive);
channel.willStartReceivingRequests();
logger.info("Server conduit/$identifier started.");
}
/// Sends an application event.
///
/// This method is designed to handle application-wide events. By default,
/// it does nothing and serves as a placeholder for potential event handling
/// implementations in derived classes.
///
/// Parameters:
/// [event]: A dynamic object representing the event to be sent.
/// It can be of any type, allowing flexibility in event structures.
///
/// Note:
/// Override this method in subclasses to implement specific event handling logic.
void sendApplicationEvent(dynamic event) {
// By default, do nothing
}
}

View file

@ -1,401 +0,0 @@
/*
* 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 'dart:io';
import 'package:protevus_openapi/documentable.dart';
import 'package:protevus_application/application.dart';
import 'package:protevus_http/http.dart';
import 'package:protevus_openapi/v3.dart';
import 'package:protevus_runtime/runtime.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
/// An abstract class that defines the behavior specific to your application.
///
/// You create a subclass of [ApplicationChannel] to initialize your application's services and define how HTTP requests are handled by your application.
/// There *must* only be one subclass in an application and it must be visible to your application library file, e.g., 'package:my_app/my_app.dart'.
///
/// You must implement [entryPoint] to define the controllers that comprise your application channel. Most applications will
/// also override [prepare] to read configuration values and initialize services. Some applications will provide an [initializeApplication]
/// method to do global startup tasks.
///
/// When your application is started, an instance of your application channel is created for each isolate (see [Application.start]). Each instance
/// is a replica of your application that runs in its own memory isolated thread.
abstract class ApplicationChannel implements APIComponentDocumenter {
/// Provides global initialization for the application.
///
/// Most of your application initialization code is written in [prepare], which is invoked for each isolate. For initialization that
/// needs to occur once per application start, you must provide an implementation for this method. This method is invoked prior
/// to any isolates being spawned.
///
/// You may alter [options] in this method and those changes will be available in each instance's [options]. To pass arbitrary data
/// to each of your isolates at startup, add that data to [ApplicationOptions.context].
///
/// Example:
///
/// class MyChannel extends ApplicationChannel {
/// static Future initializeApplication(ApplicationOptions options) async {
/// options.context["runtimeOption"] = "foo";
/// }
///
/// Future prepare() async {
/// if (options.context["runtimeOption"] == "foo") {
/// // do something
/// }
/// }
/// }
///
///
/// Do not configure objects like [CodecRegistry], [CORSPolicy.defaultPolicy] or any other value that isn't explicitly passed through [options].
///
/// * Note that static methods are not inherited in Dart and therefore you are not overriding this method. The declaration of this method in the base [ApplicationChannel] class
/// is for documentation purposes.
static Future initializeApplication(ApplicationOptions options) async {}
/// Returns a Logger instance for this object.
///
/// This logger's name appears as 'conduit'.
Logger get logger => Logger("protevus");
/// Returns the [ApplicationServer] instance that sends HTTP requests to this object.
///
/// This getter provides access to the server associated with this ApplicationChannel.
/// The server is responsible for handling incoming HTTP requests and routing them
/// to the appropriate controllers within the channel.
ApplicationServer get server => _server;
/// Sets the ApplicationServer for this channel and establishes message hub connections.
///
/// This setter method performs two main tasks:
/// 1. It assigns the provided [server] to the private [_server] variable.
/// 2. It sets up the message hub connections:
/// - It adds a listener to the outbound stream of the messageHub, which sends
/// application events through the server.
/// - It sets the inbound sink of the messageHub as the hubSink of the server.
///
/// This setup allows for inter-isolate communication through the ApplicationMessageHub.
///
/// [server] The ApplicationServer instance to be set for this channel.
set server(ApplicationServer server) {
_server = server;
messageHub._outboundController.stream.listen(server.sendApplicationEvent);
server.hubSink = messageHub._inboundController.sink;
}
/// A messaging hub for inter-isolate communication within the application.
///
/// You use this object to synchronize state across the isolates of an application. Any data sent
/// through this object will be received by every other channel in your application (except the one that sent it).
final ApplicationMessageHub messageHub = ApplicationMessageHub();
/// Returns a SecurityContext for HTTPS configuration if certificate and private key files are provided.
///
/// If this value is non-null, the [server] receiving HTTP requests will only accept requests over HTTPS.
///
/// By default, this value is null. If the [ApplicationOptions] provided to the application are configured to
/// reference a private key and certificate file, this value is derived from that information. You may override
/// this method to provide an alternative means to creating a [SecurityContext].
SecurityContext? get securityContext {
if (options?.certificateFilePath == null ||
options?.privateKeyFilePath == null) {
return null;
}
return SecurityContext()
..useCertificateChain(options!.certificateFilePath!)
..usePrivateKey(options!.privateKeyFilePath!);
}
/// The configuration options used to start the application this channel belongs to.
///
/// These options are set when starting the application. Changes to this object have no effect
/// on other isolates.
///
/// This property holds an instance of [ApplicationOptions] which contains various
/// configuration settings for the application. These options are typically set
/// during the application's startup process.
///
/// The options stored here are specific to this channel instance and do not
/// affect other isolates running in the application. This means that modifying
/// these options at runtime will only impact the current isolate.
///
/// The property is nullable, allowing for cases where options might not be set
/// or where default configurations are used in the absence of specific options.
ApplicationOptions? options;
/// You implement this accessor to define how HTTP requests are handled by your application.
///
/// You must implement this method to return the first controller that will handle an HTTP request. Additional controllers
/// are linked to the first controller to create the entire flow of your application's request handling logic. This method
/// is invoked during startup and controllers cannot be changed after it is invoked. This method is always invoked after
/// [prepare].
///
/// In most applications, the first controller is a [Router]. Example:
///
/// @override
/// Controller get entryPoint {
/// final router = Router();
/// router.route("/path").link(() => PathController());
/// return router;
/// }
Controller get entryPoint;
/// The [ApplicationServer] instance associated with this channel.
///
/// This private variable stores the server that handles HTTP requests for this
/// ApplicationChannel. It is marked as 'late' because it will be initialized
/// after the channel is created, typically when the 'server' setter is called.
///
/// The server is responsible for managing incoming HTTP connections and
/// routing requests to the appropriate controllers within the channel.
late ApplicationServer _server;
/// Performs initialization tasks for the application channel.
///
/// This method allows this instance to perform any initialization (other than setting up the [entryPoint]). This method
/// is often used to set up services that [Controller]s use to fulfill their duties. This method is invoked
/// prior to [entryPoint], so that the services it creates can be injected into [Controller]s.
///
/// By default, this method does nothing.
Future prepare() async {}
/// Overridable method called just before the application starts receiving requests.
///
/// Override this method to take action just before [entryPoint] starts receiving requests. By default, does nothing.
void willStartReceivingRequests() {}
/// Releases resources and performs cleanup when the application channel is closing.
///
/// This method is invoked when the owning [Application] is stopped. It closes open ports
/// that this channel was using so that the application can be properly shut down.
///
/// Prefer to use [ServiceRegistry] instead of overriding this method.
///
/// If you do override this method, you must call the super implementation.
@mustCallSuper
Future close() async {
logger.fine(
"ApplicationChannel(${server.identifier}).close: closing messageHub",
);
await messageHub.close();
}
/// Creates an OpenAPI document for the components and paths in this channel.
///
/// This method generates a complete OpenAPI specification document for the application,
/// including all components, paths, and operations defined in the channel.
///
/// The documentation process first invokes [documentComponents] on this channel. Every controller in the channel will have its
/// [documentComponents] methods invoked. Any declared property
/// of this channel that implements [APIComponentDocumenter] will have its [documentComponents]
/// method invoked. If there services that are part of the application, but not stored as properties of this channel, you may override
/// [documentComponents] in your subclass to add them. You must call the superclass' implementation of [documentComponents].
///
/// After components have been documented, [APIOperationDocumenter.documentPaths] is invoked on [entryPoint]. The controllers
/// of the channel will add paths and operations to the document during this process.
///
/// This method should not be overridden.
///
/// [projectSpec] should contain the keys `name`, `version` and `description`.
Future<APIDocument> documentAPI(Map<String, dynamic> projectSpec) async {
final doc = APIDocument()..components = APIComponents();
final root = entryPoint;
root.didAddToChannel();
final context = APIDocumentContext(doc);
documentComponents(context);
doc.paths = root.documentPaths(context);
doc.info = APIInfo(
projectSpec["name"] as String?,
projectSpec["version"] as String?,
description: projectSpec["description"] as String?,
);
await context.finalize();
return doc;
}
/// Documents the components of this ApplicationChannel and its controllers.
///
/// This method is responsible for generating API documentation for the components
/// of this ApplicationChannel and its associated controllers. It performs the following tasks:
///
/// 1. Calls `documentComponents` on the entry point controller, which typically
/// initiates the documentation process for all linked controllers.
///
/// 2. Retrieves all documentable channel components using the ChannelRuntime,
/// which are typically services or other objects that implement APIComponentDocumenter.
///
/// 3. Calls `documentComponents` on each of these channel components, allowing
/// them to add their own documentation to the API registry.
///
/// This method is marked with @mustCallSuper, indicating that subclasses
/// overriding this method must call the superclass implementation.
///
/// [registry] The APIDocumentContext used to store and organize the API documentation.
@mustCallSuper
@override
void documentComponents(APIDocumentContext registry) {
entryPoint.documentComponents(registry);
(RuntimeContext.current[runtimeType] as ChannelRuntime)
.getDocumentableChannelComponents(this)
.forEach((component) {
component.documentComponents(registry);
});
}
}
/// An object that facilitates message passing between [ApplicationChannel]s in different isolates.
///
/// You use this object to share information between isolates. Each [ApplicationChannel] has a property of this type. A message sent through this object
/// is received by every other channel through its hub.
///
/// To receive messages in a hub, add a listener via [listen]. To send messages, use [add].
///
/// For example, an application may want to send data to every connected websocket. A reference to each websocket
/// is only known to the isolate it established a connection on. This data must be sent to each isolate so that each websocket
/// connected to that isolate can send the data:
///
/// router.route("/broadcast").linkFunction((req) async {
/// var message = await req.body.decodeAsString();
/// websocketsOnThisIsolate.forEach((s) => s.add(message);
/// messageHub.add({"event": "broadcastMessage", "data": message});
/// return Response.accepted();
/// });
///
/// messageHub.listen((event) {
/// if (event is Map && event["event"] == "broadcastMessage") {
/// websocketsOnThisIsolate.forEach((s) => s.add(event["data"]);
/// }
/// });
class ApplicationMessageHub extends Stream<dynamic> implements Sink<dynamic> {
/// A logger instance for the ApplicationMessageHub.
///
/// This logger is used to log messages and errors related to the ApplicationMessageHub.
/// It is named "protevus" to identify logs from this specific component.
final Logger _logger = Logger("protevus");
/// A StreamController for outbound messages.
///
/// This controller manages the stream of outbound messages sent from this
/// ApplicationMessageHub to other hubs. It is used internally to handle
/// the flow of messages being sent out to other isolates.
///
/// The stream is not broadcast, meaning it only allows a single subscriber.
/// This is typically used by the ApplicationServer to listen for outbound
/// messages and distribute them to other isolates.
final StreamController<dynamic> _outboundController =
StreamController<dynamic>();
/// A StreamController for inbound messages.
///
/// This controller manages the stream of inbound messages received by this
/// ApplicationMessageHub from other hubs. It is used internally to handle
/// the flow of messages coming in from other isolates.
///
/// The stream is broadcast, meaning it allows multiple subscribers. This allows
/// multiple parts of the application to listen for and react to incoming messages
/// independently.
final StreamController<dynamic> _inboundController =
StreamController<dynamic>.broadcast();
/// Adds a listener for messages from other hubs.
///
/// A class that facilitates message passing between [ApplicationChannel]s in different isolates.
///
/// This class implements both [Stream] and [Sink] interfaces, allowing it to send and receive messages
/// across isolates. It uses separate controllers for inbound and outbound messages to manage the flow
/// of data.
///
/// You use this method to add listeners for messages from other hubs.
/// When another hub [add]s a message, this hub will receive it on [onData].
///
/// [onError], if provided, will be invoked when this isolate tries to [add] invalid data. Only the isolate
/// that failed to send the data will receive [onError] events.
@override
StreamSubscription<dynamic> listen(
void Function(dynamic event)? onData, {
Function? onError,
void Function()? onDone,
bool? cancelOnError = false,
}) =>
_inboundController.stream.listen(
onData,
onError: onError ??
((err, StackTrace st) =>
_logger.severe("ApplicationMessageHub error", err, st)),
onDone: onDone,
cancelOnError: cancelOnError,
);
/// Sends a message to all other hubs.
///
/// This method allows sending a message [event] to all other isolates in the application.
/// The message will be delivered to all other isolates that have set up a callback using [listen].
///
/// [event] must be isolate-safe data - in general, this means it may not be or contain a closure. Consult the API reference `dart:isolate` for more details. If [event]
/// is not isolate-safe data, an error is delivered to [listen] on this isolate.
@override
void add(dynamic event) {
_outboundController.sink.add(event);
}
/// Closes the message hub and its associated stream controllers.
///
/// This method performs the following tasks:
/// 1. If the outbound controller has no listeners, it adds a dummy listener
/// to prevent potential issues with unhandled stream events.
/// 2. If the inbound controller has no listeners, it adds a dummy listener
/// for the same reason.
/// 3. Closes both the outbound and inbound controllers.
///
/// This method should be called when the application is shutting down or
/// when the message hub is no longer needed to ensure proper cleanup of resources.
///
/// Returns a Future that completes when both controllers have been closed.
@override
Future close() async {
if (!_outboundController.hasListener) {
_outboundController.stream.listen(null);
}
if (!_inboundController.hasListener) {
_inboundController.stream.listen(null);
}
await _outboundController.close();
await _inboundController.close();
}
}
/// An abstract class that defines the runtime behavior of an ApplicationChannel.
///
/// This class provides methods and properties for managing the lifecycle,
/// documentation, and instantiation of an ApplicationChannel.
abstract class ChannelRuntime {
Iterable<APIComponentDocumenter> getDocumentableChannelComponents(
ApplicationChannel channel,
);
Type get channelType;
String get name;
Uri get libraryUri;
IsolateEntryFunction get isolateEntryPoint;
ApplicationChannel instantiateChannel();
Future runGlobalInitialization(ApplicationOptions config);
}

View file

@ -1,255 +0,0 @@
/*
* 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 'dart:isolate';
import 'package:logging/logging.dart';
import 'package:protevus_application/application.dart';
/// An isolated server implementation of the ApplicationServer class.
///
/// This class extends ApplicationServer to run in a separate isolate, allowing
/// for concurrent execution of multiple server instances. It manages communication
/// with a supervising application through message passing.
///
/// The server can be started, stopped, and can send and receive application events.
/// It also supports optional console logging for debugging purposes.
///
/// Constructor parameters:
/// - channelType: The type of channel to be used.
/// - configuration: ApplicationOptions for server configuration.
/// - identifier: A unique identifier for this server instance.
/// - supervisingApplicationPort: SendPort for communicating with the supervising application.
/// - logToConsole: Optional flag to enable console logging (default is false).
class ApplicationIsolateServer extends ApplicationServer {
/// Constructor for ApplicationIsolateServer.
///
/// Creates a new instance of ApplicationIsolateServer with the specified parameters.
///
/// Parameters:
/// - channelType: The type of channel to be used for communication.
/// - configuration: ApplicationOptions for configuring the server.
/// - identifier: A unique identifier for this server instance.
/// - supervisingApplicationPort: SendPort for communicating with the supervising application.
/// - logToConsole: Optional flag to enable console logging (default is false).
///
/// This constructor initializes the server, sets up logging if enabled, and establishes
/// communication with the supervising application. It also sets up a listener for
/// incoming messages from the supervisor.
ApplicationIsolateServer(
Type channelType,
ApplicationOptions configuration,
int identifier,
this.supervisingApplicationPort, {
bool logToConsole = false,
}) : super(channelType, configuration, identifier) {
if (logToConsole) {
hierarchicalLoggingEnabled = true;
logger.level = Level.ALL;
// ignore: avoid_print
logger.onRecord.listen(print);
}
supervisingReceivePort = ReceivePort();
supervisingReceivePort.listen(listener);
logger
.fine("ApplicationIsolateServer($identifier) listening, sending port");
supervisingApplicationPort.send(supervisingReceivePort.sendPort);
}
/// A SendPort used for communication with the supervising application.
///
/// This SendPort allows the ApplicationIsolateServer to send messages and events
/// back to the supervising application, enabling bidirectional communication
/// between the isolated server and its parent process.
SendPort supervisingApplicationPort;
/// A ReceivePort for receiving messages from the supervising application.
///
/// This ReceivePort is used to listen for incoming messages from the supervising
/// application. It's initialized in the constructor and is used to set up a
/// listener for handling various commands and messages, such as stop requests
/// or application events.
///
/// The 'late' keyword indicates that this variable will be initialized after
/// the constructor body, but before it's used.
late ReceivePort supervisingReceivePort;
/// Starts the ApplicationIsolateServer.
///
/// This method overrides the base class's start method to add functionality
/// specific to the isolated server. It performs the following steps:
/// 1. Calls the superclass's start method with the provided shareHttpServer parameter.
/// 2. Logs a fine-level message indicating that the server has started.
/// 3. Sends a 'listening' message to the supervising application.
///
/// Parameters:
/// - shareHttpServer: A boolean indicating whether to share the HTTP server (default is false).
///
/// Returns:
/// A Future that completes with the result of the superclass's start method.
///
/// Throws:
/// Any exceptions that may be thrown by the superclass's start method.
@override
Future start({bool shareHttpServer = false}) async {
final result = await super.start(shareHttpServer: shareHttpServer);
logger.fine(
"ApplicationIsolateServer($identifier) started, sending listen message",
);
supervisingApplicationPort
.send(ApplicationIsolateSupervisor.messageKeyListening);
return result;
}
/// Sends an application event to the supervising application.
///
/// This method overrides the base class's sendApplicationEvent method to
/// implement event sending in the context of an isolated server. It wraps
/// the event in a MessageHubMessage and sends it through the
/// supervisingApplicationPort.
///
/// Parameters:
/// - event: The application event to be sent. Can be of any type.
///
/// If an error occurs during the sending process, it is caught and added
/// to the hubSink as an error, along with the stack trace.
///
/// Note: This method does not throw exceptions directly; instead, it
/// reports errors through the hubSink.
@override
void sendApplicationEvent(dynamic event) {
try {
supervisingApplicationPort.send(MessageHubMessage(event));
} catch (e, st) {
hubSink?.addError(e, st);
}
}
/// Listener method for handling incoming messages from the supervising application.
///
/// This method processes two types of messages:
/// 1. A stop message (ApplicationIsolateSupervisor.messageKeyStop):
/// When received, it calls the stop() method to shut down the server.
/// 2. A MessageHubMessage:
/// When received, it adds the payload of the message to the hubSink.
///
/// Parameters:
/// - message: The incoming message. Can be either a stop command or a MessageHubMessage.
///
/// This method doesn't return any value but performs actions based on the message type:
/// - For a stop message, it initiates the server shutdown process.
/// - For a MessageHubMessage, it propagates the payload to the hubSink if it exists.
///
/// Note: This method assumes that hubSink is properly initialized elsewhere in the class.
void listener(dynamic message) {
if (message == ApplicationIsolateSupervisor.messageKeyStop) {
stop();
} else if (message is MessageHubMessage) {
hubSink?.add(message.payload);
}
}
/// Stops the ApplicationIsolateServer and performs cleanup operations.
///
/// This method performs the following steps:
/// 1. Closes the supervisingReceivePort to stop receiving messages.
/// 2. Logs a fine-level message indicating the server is closing.
/// 3. Calls the close() method to shut down the server.
/// 4. Logs a fine-level message confirming the server has closed.
/// 5. Clears all listeners from the logger.
/// 6. Logs a fine-level message indicating it's sending a stop acknowledgement.
/// 7. Sends a stop acknowledgement message to the supervising application.
///
/// Returns:
/// A Future that completes when all stop operations are finished.
///
/// Note: This method is asynchronous and should be awaited when called.
Future stop() async {
supervisingReceivePort.close();
logger.fine("ApplicationIsolateServer($identifier) closing server");
await close();
logger.fine("ApplicationIsolateServer($identifier) did close server");
logger.clearListeners();
logger.fine(
"ApplicationIsolateServer($identifier) sending stop acknowledgement",
);
supervisingApplicationPort
.send(ApplicationIsolateSupervisor.messageKeyStop);
}
}
/// A typedef defining the signature for an isolate entry function.
///
/// This function type is used to define the entry point for an isolate in the context
/// of an ApplicationIsolateServer. It takes a single parameter of type
/// ApplicationInitialServerMessage, which contains all the necessary information
/// to initialize and run the server within the isolate.
///
/// Parameters:
/// - message: An ApplicationInitialServerMessage object containing configuration
/// details, identifiers, and communication ports needed to set up the server
/// in the isolate.
///
/// This typedef is typically used when spawning new isolates for server instances,
/// allowing for a standardized way of passing initial setup information to the isolate.
typedef IsolateEntryFunction = void Function(
ApplicationInitialServerMessage message,
);
/// Represents the initial message sent to an ApplicationIsolateServer when it's created.
///
/// This class encapsulates all the necessary information needed to initialize and
/// configure an ApplicationIsolateServer within an isolate. It includes details about
/// the stream type, configuration options, communication ports, and logging preferences.
///
/// Properties:
/// - streamTypeName: The name of the stream type to be used by the server.
/// - streamLibraryURI: The URI of the library containing the stream implementation.
/// - configuration: ApplicationOptions object containing server configuration details.
/// - parentMessagePort: SendPort for communicating with the parent (supervising) application.
/// - identifier: A unique identifier for the server instance.
/// - logToConsole: A boolean flag indicating whether to enable console logging (default is false).
///
/// This class is typically used when spawning a new isolate for an ApplicationIsolateServer,
/// providing all the necessary information in a single, structured message.
class ApplicationInitialServerMessage {
ApplicationInitialServerMessage(
this.streamTypeName,
this.streamLibraryURI,
this.configuration,
this.identifier,
this.parentMessagePort, {
this.logToConsole = false,
});
String streamTypeName;
Uri streamLibraryURI;
ApplicationOptions configuration;
SendPort parentMessagePort;
int identifier;
bool logToConsole = false;
}
/// Represents a message that can be sent through the message hub.
///
/// This class encapsulates a payload of any type, allowing for flexible
/// communication between different parts of the application.
///
/// The payload can be of any type (dynamic), making this class versatile
/// for various types of messages.
///
/// Parameters:
/// - payload: The content of the message, which can be of any type.
class MessageHubMessage {
MessageHubMessage(this.payload);
dynamic payload;
}

View file

@ -1,386 +0,0 @@
/*
* 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 'dart:isolate';
import 'package:protevus_application/application.dart';
import 'package:logging/logging.dart';
/// Represents the supervision of a [ApplicationIsolateServer].
///
/// This class, ApplicationIsolateSupervisor, is responsible for supervising and managing
/// an [ApplicationIsolateServer]. It handles the lifecycle of the isolate, including
/// starting, stopping, and communicating with it. The supervisor also manages error
/// handling, message passing between isolates, and ensures proper startup and shutdown
/// of the supervised isolate.
///
/// You should not use this class directly.
class ApplicationIsolateSupervisor {
/// Creates an instance of [ApplicationIsolateSupervisor].
///
/// This constructor initializes a new [ApplicationIsolateSupervisor] with the provided parameters.
///
/// Parameters:
/// - [supervisingApplication]: The [Application] instance that owns this supervisor.
/// - [isolate]: The [Isolate] being supervised.
/// - [receivePort]: The [ReceivePort] for receiving messages from the supervised isolate.
/// - [identifier]: A numeric identifier for the isolate relative to the [Application].
/// - [logger]: The [Logger] instance used for logging.
/// - [startupTimeout]: Optional. The maximum duration to wait for the isolate to start up.
/// Defaults to 30 seconds.
ApplicationIsolateSupervisor(
this.supervisingApplication,
this.isolate,
this.receivePort,
this.identifier,
this.logger, {
this.startupTimeout = const Duration(seconds: 30),
});
/// The [Isolate] being supervised by this [ApplicationIsolateSupervisor].
///
/// This isolate represents a separate thread of execution where the
/// [ApplicationIsolateServer] runs. The supervisor manages the lifecycle
/// and communication with this isolate.
final Isolate isolate;
/// The [ReceivePort] for receiving messages from the supervised isolate.
///
/// This [ReceivePort] is used to establish a communication channel between
/// the supervisor and the supervised isolate. It allows the supervisor to
/// receive various types of messages, including startup notifications,
/// error reports, and custom messages from the isolate.
///
/// The [receivePort] is crucial for managing the lifecycle of the isolate
/// and handling inter-isolate communication. It is used in conjunction with
/// the [listener] method to process incoming messages from the isolate.
final ReceivePort receivePort;
/// A numeric identifier for the isolate relative to the [Application].
///
/// This identifier is unique within the context of the parent [Application].
/// It is used to distinguish between different isolates managed by the same
/// application, facilitating logging, debugging, and isolate management.
/// The identifier is typically assigned sequentially when creating new isolates.
final int identifier;
/// The maximum duration to wait for the isolate to start up.
///
/// This duration specifies the time limit for the supervised isolate to complete its
/// startup process. If the isolate fails to start within this timeout period, an
/// exception will be thrown. This helps prevent indefinite waiting in case of startup
/// issues.
///
/// The default value is typically set in the constructor, often to 30 seconds.
final Duration startupTimeout;
/// A reference to the owning [Application].
///
/// This property holds a reference to the [Application] instance that owns and manages
/// this [ApplicationIsolateSupervisor]. It allows the supervisor to interact with
/// the main application, access shared resources, and coordinate activities across
/// multiple isolates.
///
/// The supervising application is responsible for creating and managing the lifecycle
/// of this supervisor and its associated isolate. It also provides context for
/// operations such as logging, configuration, and inter-isolate communication.
Application supervisingApplication;
/// The logger instance used for recording events and errors.
///
/// This [Logger] is typically shared with the [supervisingApplication] and is used
/// to log various events, warnings, and errors related to the isolate supervision
/// process. It helps in debugging and monitoring the behavior of the supervised
/// isolate and the supervisor itself.
///
/// The logger can be used to record information at different severity levels,
/// such as fine, info, warning, and severe, depending on the nature of the event
/// being logged.
Logger logger;
/// A list to store pending [MessageHubMessage] objects.
///
/// This queue is used to temporarily hold messages that are received when the
/// supervising application is not running. Once the application starts running,
/// these messages are processed and sent to other supervisors.
///
/// The queue helps ensure that no messages are lost during the startup phase
/// of the application, maintaining message integrity across isolates.
final List<MessageHubMessage> _pendingMessageQueue = [];
/// Indicates whether the isolate is currently in the process of launching.
///
/// This getter returns `true` if the isolate is still in the startup phase,
/// and `false` if the launch process has completed.
///
/// It checks the state of the [_launchCompleter] to determine if the
/// launch process is still ongoing. If the completer is not yet completed,
/// it means the isolate is still launching.
///
/// This property is useful for handling different behaviors or error states
/// depending on whether the isolate is in its launch phase or has already
/// started running normally.
bool get _isLaunching => !_launchCompleter.isCompleted;
/// The [SendPort] used to send messages to the supervised isolate.
///
/// This [SendPort] is initialized when the supervisor receives the corresponding
/// [SendPort] from the supervised isolate during the startup process. It enables
/// bi-directional communication between the supervisor and the supervised isolate.
///
/// The [_serverSendPort] is used to send various messages to the isolate, including
/// stop signals, custom application messages, and other control commands. It plays
/// a crucial role in managing the lifecycle and behavior of the supervised isolate.
late SendPort _serverSendPort;
/// A [Completer] used to manage the launch process of the supervised isolate.
///
/// This completer is initialized when the isolate is being launched and is completed
/// when the isolate has successfully started up. It's used in conjunction with
/// [resume] method to handle the asynchronous nature of isolate startup.
///
/// The completer allows other parts of the supervisor to wait for the isolate
/// to finish launching before proceeding with further operations. It's also used
/// to implement timeout functionality in case the isolate fails to start within
/// the specified [startupTimeout].
late Completer _launchCompleter;
/// A [Completer] used to manage the stop process of the supervised isolate.
///
/// This nullable [Completer] is initialized when the [stop] method is called
/// and is used to handle the asynchronous nature of stopping the isolate.
/// It allows the supervisor to wait for the isolate to acknowledge the stop
/// message before proceeding with the termination process.
///
/// The completer is set to null after the stop process is complete, indicating
/// that the isolate has been successfully stopped. This helps manage the state
/// of the stop operation and prevents multiple stop attempts from interfering
/// with each other.
Completer? _stopCompleter;
/// A constant string used as a message key to signal the supervised isolate to stop.
///
/// This constant is used in the communication protocol between the supervisor
/// and the supervised isolate. When sent to the isolate, it indicates that
/// the isolate should begin its shutdown process.
///
/// The underscore prefix in the value suggests that this is intended for
/// internal use within the isolate communication system.
static const String messageKeyStop = "_MessageStop";
/// A constant string used as a message key to indicate that the supervised isolate is listening.
///
/// This constant is part of the communication protocol between the supervisor
/// and the supervised isolate. When the isolate sends this message to the
/// supervisor, it signals that the isolate has completed its startup process
/// and is ready to receive and process messages.
///
/// The underscore prefix in the value suggests that this is intended for
/// internal use within the isolate communication system.
static const String messageKeyListening = "_MessageListening";
/// Resumes the [Isolate] being supervised.
///
/// This method initiates the process of resuming the supervised isolate and
/// sets up the necessary listeners and error handlers. It performs the following steps:
///
/// 1. Initializes a new [Completer] for managing the launch process.
/// 2. Sets up a listener for the [receivePort] to handle incoming messages.
/// 3. Configures the isolate to handle errors non-fatally.
/// 4. Adds an error listener to the isolate.
/// 5. Resumes the isolate from its paused state.
/// 6. Waits for the isolate to complete its startup process.
///
/// If the isolate fails to start within the specified [startupTimeout], a
/// [TimeoutException] is thrown with a detailed error message.
///
/// Returns a [Future] that completes when the isolate has successfully started,
/// or throws an exception if the startup process fails or times out.
///
/// Throws:
/// - [TimeoutException] if the isolate doesn't start within the [startupTimeout].
Future resume() {
_launchCompleter = Completer();
receivePort.listen(listener);
isolate.setErrorsFatal(false);
isolate.addErrorListener(receivePort.sendPort);
logger.fine(
"ApplicationIsolateSupervisor($identifier).resume will resume isolate",
);
isolate.resume(isolate.pauseCapability!);
return _launchCompleter.future.timeout(
startupTimeout,
onTimeout: () {
logger.fine(
"ApplicationIsolateSupervisor($identifier).resume timed out waiting for isolate start",
);
throw TimeoutException(
"Isolate ($identifier) failed to launch in $startupTimeout seconds. "
"There may be an error with your application or Application.isolateStartupTimeout needs to be increased.");
},
);
}
/// Stops the [Isolate] being supervised.
///
/// This method initiates the process of stopping the supervised isolate. It performs the following steps:
///
/// 1. Creates a new [Completer] to manage the stop process.
/// 2. Sends a stop message to the supervised isolate using [_serverSendPort].
/// 3. Waits for the isolate to acknowledge the stop message.
/// 4. If the isolate doesn't respond within 5 seconds, logs a severe message.
/// 5. Forcefully kills the isolate using [isolate.kill()].
/// 6. Closes the [receivePort] to clean up resources.
///
/// The method uses a timeout of 5 seconds to wait for the isolate's acknowledgment.
/// If the timeout occurs, it assumes the isolate is not responding and proceeds to terminate it.
///
/// This method ensures that the isolate is stopped one way or another, either gracefully
/// or by force, and properly cleans up associated resources.
///
/// Returns a [Future] that completes when the stop process is finished, regardless of
/// whether the isolate responded to the stop message or was forcefully terminated.
Future stop() async {
_stopCompleter = Completer();
logger.fine(
"ApplicationIsolateSupervisor($identifier).stop sending stop to supervised isolate",
);
_serverSendPort.send(messageKeyStop);
try {
await _stopCompleter!.future.timeout(const Duration(seconds: 5));
} on TimeoutException {
logger.severe(
"Isolate ($identifier) not responding to stop message, terminating.",
);
} finally {
isolate.kill();
}
receivePort.close();
}
/// Handles incoming messages from the supervised isolate.
///
/// This method is the central message processing function for the supervisor.
/// It handles various types of messages:
///
/// - [SendPort]: Stores the send port for communicating with the isolate.
/// - [messageKeyListening]: Indicates the isolate has started and is listening.
/// - [messageKeyStop]: Acknowledges that the isolate has received a stop message.
/// - [List]: Represents an error from the isolate, which is then handled.
/// - [MessageHubMessage]: Inter-isolate communication message.
///
/// For [MessageHubMessage], if the supervising application is not running,
/// the message is queued. Otherwise, it's immediately sent to other supervisors.
///
/// This method is crucial for managing the lifecycle and communication of the
/// supervised isolate, handling startup, shutdown, errors, and inter-isolate messaging.
void listener(dynamic message) {
if (message is SendPort) {
_serverSendPort = message;
} else if (message == messageKeyListening) {
_launchCompleter.complete();
logger.fine(
"ApplicationIsolateSupervisor($identifier) isolate listening acknowledged",
);
} else if (message == messageKeyStop) {
logger.fine(
"ApplicationIsolateSupervisor($identifier) stop message acknowledged",
);
receivePort.close();
_stopCompleter!.complete();
_stopCompleter = null;
} else if (message is List) {
logger.fine(
"ApplicationIsolateSupervisor($identifier) received isolate error ${message.first}",
);
final stacktrace = StackTrace.fromString(message.last as String);
_handleIsolateException(message.first, stacktrace);
} else if (message is MessageHubMessage) {
if (!supervisingApplication.isRunning) {
_pendingMessageQueue.add(message);
} else {
_sendMessageToOtherSupervisors(message);
}
}
}
/// Sends all pending messages stored in the [_pendingMessageQueue] to other supervisors.
///
/// This method is typically called when the supervising application starts running
/// to process any messages that were received while the application was not active.
/// It performs the following steps:
///
/// 1. Creates a copy of the [_pendingMessageQueue] to safely iterate over it.
/// 2. Clears the original [_pendingMessageQueue].
/// 3. Sends each message in the copied list to other supervisors using [_sendMessageToOtherSupervisors].
///
/// This ensures that no messages are lost during the startup phase of the application
/// and maintains message integrity across isolates.
void sendPendingMessages() {
final list = List<MessageHubMessage>.from(_pendingMessageQueue);
_pendingMessageQueue.clear();
list.forEach(_sendMessageToOtherSupervisors);
}
/// Sends a [MessageHubMessage] to all other supervisors managed by the supervising application.
///
/// This method is responsible for propagating messages across different isolates
/// managed by the same application. It performs the following actions:
///
/// 1. Iterates through all supervisors in the supervising application.
/// 2. Excludes the current supervisor from the recipients.
/// 3. Sends the provided [message] to each of the other supervisors' isolates.
///
/// This method is crucial for maintaining communication and synchronization
/// between different isolates within the application.
///
/// Parameters:
/// - [message]: The [MessageHubMessage] to be sent to other supervisors.
void _sendMessageToOtherSupervisors(MessageHubMessage message) {
supervisingApplication.supervisors
.where((sup) => sup != this)
.forEach((supervisor) {
supervisor._serverSendPort.send(message);
});
}
/// Handles exceptions thrown by the supervised isolate.
///
/// This method is responsible for processing and responding to exceptions
/// that occur within the supervised isolate. It behaves differently depending
/// on whether the isolate is still in the process of launching or not:
///
/// - If the isolate is launching ([_isLaunching] is true):
/// It wraps the error in an [ApplicationStartupException] and completes
/// the [_launchCompleter] with this error, effectively failing the launch process.
///
/// - If the isolate has already launched:
/// It logs the error as a severe uncaught exception using the supervisor's logger.
///
/// Parameters:
/// - [error]: The error or exception object thrown by the isolate.
/// - [stacktrace]: The [StackTrace] associated with the error.
///
/// This method is crucial for maintaining the stability and error handling
/// of the isolate, especially during its startup phase.
void _handleIsolateException(dynamic error, StackTrace stacktrace) {
if (_isLaunching) {
final appException = ApplicationStartupException(error);
_launchCompleter.completeError(appException, stacktrace);
} else {
logger.severe("Uncaught exception in isolate.", error, stacktrace);
}
}
}

View file

@ -1,175 +0,0 @@
/*
* 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:io';
import 'package:args/args.dart';
import 'package:protevus_application/application.dart';
/// An object that contains configuration values for an [Application].
///
/// This class provides a set of options that can be used to configure an application,
/// including network settings, SSL configuration, and custom context values.
/// It also includes a static [ArgParser] for parsing command-line arguments.
///
/// Key features:
/// - Configurable address and port for HTTP requests
/// - IPv6 support
/// - SSL/HTTPS configuration options
/// - Client-side certificate usage flag
/// - Custom context for application-specific configuration
/// - Command-line argument parsing for easy configuration
///
/// Usage:
/// This class is typically used in conjunction with [ApplicationChannel] to set up
/// and configure an application based on external inputs or command-line arguments.
class ApplicationOptions {
/// The absolute path of the configuration file for this application.
///
/// This property stores the file path of the configuration file used by the application.
/// The path is typically set when the application is started using the `--config-path` option
/// with the `conduit serve` command.
///
/// The configuration file can contain application-specific settings and can be loaded
/// in the [ApplicationChannel] to access these configuration values.
///
/// This property may be null if no configuration file path was specified when starting the application.
///
/// Usage:
/// - Access this property to get the path of the configuration file.
/// - Use the path to load and parse the configuration file in your application logic.
/// - Ensure to handle cases where this property might be null.
String? configurationFilePath;
/// The address to listen for HTTP requests on.
///
/// This property specifies the network address on which the application will listen for incoming HTTP requests.
///
/// This value may be an [InternetAddress] or a [String].
dynamic address;
/// The port number on which the application will listen for HTTP requests.
///
/// Defaults to 8888.
int port = 8888;
/// Whether or not the application should only receive connections over IPv6.
///
/// This flag determines if the application should exclusively use IPv6 for incoming connections.
/// When set to true, the application will only accept IPv6 connections and reject IPv4 connections.
/// This setting can be useful in environments that require IPv6-only communication.
///
/// Defaults to false. This flag impacts the default value of the [address] property.
bool isIpv6Only = false;
/// Indicates whether the application's request controllers should use client-side HTTPS certificates.
///
/// Defaults to false.
bool isUsingClientCertificate = false;
/// The path to a SSL certificate file.
///
/// If specified - along with [privateKeyFilePath] - an [Application] will only allow secure connections over HTTPS.
/// This value is often set through the `--ssl-certificate-path` command line option of `conduit serve`. For finer control
/// over how HTTPS is configured for an application, see [ApplicationChannel.securityContext].
String? certificateFilePath;
/// The path to a private key file for SSL/TLS encryption.
///
/// If specified - along with [certificateFilePath] - an [Application] will only allow secure connections over HTTPS.
/// This value is often set through the `--ssl-key-path` command line option of `conduit serve`. For finer control
/// over how HTTPS is configured for an application, see [ApplicationChannel.securityContext].
String? privateKeyFilePath;
/// Contextual configuration values for each [ApplicationChannel].
///
/// This is a user-specific set of configuration options provided by [ApplicationChannel.initializeApplication].
/// Each instance of [ApplicationChannel] has access to these values if set.
///
/// This map allows for storing and retrieving custom configuration values that can be used
/// throughout the application. It provides a flexible way to pass application-specific
/// settings to different parts of the system.
///
/// The context can be populated during the application's initialization phase and
/// can contain any type of data that adheres to the dynamic type.
///
/// Usage:
/// - Add configuration values: `context['databaseUrl'] = 'postgres://...';`
/// - Retrieve values: `final dbUrl = options.context['databaseUrl'];`
///
/// Note: It's important to ensure type safety when retrieving values from this map,
/// as it uses dynamic typing.
final Map<String, dynamic> context = {};
/// A static [ArgParser] for parsing command-line arguments for the application.
///
/// This parser defines several options and flags that can be used to configure
/// the application when it is launched from the command line. The available
/// options include:
///
/// - address: The address to listen on for HTTP requests.
/// - config-path: The path to a configuration file.
/// - isolates: Number of isolates for handling requests.
/// - port: The port number to listen for HTTP requests on.
/// - ipv6-only: Flag to limit listening to IPv6 connections only.
/// - ssl-certificate-path: The path to an SSL certificate file.
/// - ssl-key-path: The path to an SSL private key file.
/// - timeout: Number of seconds to wait to ensure startup succeeded.
/// - help: Flag to display help information.
///
/// Each option is configured with a description, and some include default values
/// or abbreviations for easier command-line usage.
static final parser = ArgParser()
..addOption(
"address",
abbr: "a",
help: "The address to listen on. See HttpServer.bind for more details."
" Using the default will listen on any address.",
)
..addOption(
"config-path",
abbr: "c",
help:
"The path to a configuration file. This File is available in the ApplicationOptions "
"for a ApplicationChannel to use to read application-specific configuration values. Relative paths are relative to [directory].",
defaultsTo: "config.yaml",
)
..addOption(
"isolates",
abbr: "n",
help: "Number of isolates handling requests.",
)
..addOption(
"port",
abbr: "p",
help: "The port number to listen for HTTP requests on.",
defaultsTo: "8888",
)
..addFlag(
"ipv6-only",
help: "Limits listening to IPv6 connections only.",
negatable: false,
)
..addOption(
"ssl-certificate-path",
help:
"The path to an SSL certicate file. If provided along with --ssl-certificate-path, the application will be HTTPS-enabled.",
)
..addOption(
"ssl-key-path",
help:
"The path to an SSL private key file. If provided along with --ssl-certificate-path, the application will be HTTPS-enabled.",
)
..addOption(
"timeout",
help: "Number of seconds to wait to ensure startup succeeded.",
defaultsTo: "45",
)
..addFlag("help");
}

View file

@ -1,56 +0,0 @@
/*
* 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 'dart:isolate';
import 'package:protevus_application/application.dart';
/*
Warning: do not remove. This method is invoked by a generated script.
*/
/// Starts the application either on the current isolate or across multiple isolates.
///
/// This function initializes and starts the application, setting up communication
/// between isolates using ports. It responds to stop commands and reports the
/// application's status back to the parent isolate.
///
/// Parameters:
/// - app: The Application instance to be started.
/// - isolateCount: The number of isolates to start the application on. If 0, starts on the current isolate.
/// - parentPort: The SendPort of the parent isolate for communication.
///
/// The function sets up a ReceivePort to listen for commands, particularly the "stop" command.
/// It then starts the application either on the current isolate or across multiple isolates
/// based on the isolateCount parameter. Finally, it sends a status message back to the parent isolate.
Future startApplication<T extends ApplicationChannel>(
Application<T> app,
int isolateCount,
SendPort parentPort,
) async {
final port = ReceivePort();
port.listen((msg) {
if (msg["command"] == "stop") {
port.close();
app.stop().then((_) {
parentPort.send({"status": "stopped"});
});
}
});
if (isolateCount == 0) {
await app.startOnCurrentIsolate();
} else {
await app.start(numberOfInstances: isolateCount);
}
parentPort.send({"status": "ok", "port": port.sendPort});
}

View file

@ -1,23 +0,0 @@
name: protevus_application
description: The Application Package for the Protevus Platform
version: 0.0.1
homepage: https://protevus.com
documentation: https://docs.protevus.com
repository: https://git.protevus.com/protevus/platform
environment:
sdk: ^3.4.2
# Add regular dependencies here.
dependencies:
args: ^2.4.2
logging: ^1.2.0
meta: ^1.12.0
protevus_runtime: ^0.0.1
protevus_openapi: ^0.0.1
protevus_http: ^0.0.1
# path: ^1.8.0
dev_dependencies:
lints: ^3.0.0
test: ^1.24.0

View file

@ -1,7 +0,0 @@
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/
# Avoid committing pubspec.lock for library packages; see
# https://dart.dev/guides/libraries/private-files#pubspeclock.
pubspec.lock

View file

@ -1,3 +0,0 @@
## 1.0.0
- Initial version.

View file

@ -1,10 +0,0 @@
The MIT License (MIT)
The Laravel Framework is Copyright (c) Taylor Otwell
The Fabric Framework is Copyright (c) Vieo, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1 +0,0 @@
<p align="center"><a href="https://protevus.com" target="_blank"><img src="https://git.protevus.com/protevus/branding/raw/branch/main/protevus-logo-bg.png"></a></p>

View file

@ -1,30 +0,0 @@
# This file configures the static analysis results for your project (errors,
# warnings, and lints).
#
# This enables the 'recommended' set of lints from `package:lints`.
# This set helps identify many issues that may lead to problems when running
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
# style and format.
#
# If you want a smaller set of lints you can change this to specify
# 'package:lints/core.yaml'. These are just the most critical lints
# (the recommended set includes the core lints).
# The core lints are also what is used by pub.dev for scoring packages.
include: package:lints/recommended.yaml
# Uncomment the following section to specify additional rules.
# linter:
# rules:
# - camel_case_types
# analyzer:
# exclude:
# - path/to/excluded/files/**
# For more information about the core and recommended set of lints, see
# https://dart.dev/go/core-lints
# For additional information about configuring this file, see
# https://dart.dev/guides/language/analysis-options

View file

@ -1,80 +0,0 @@
/*
* 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_auth/auth.dart';
import 'package:protevus_hashing/hashing.dart';
import 'package:crypto/crypto.dart';
export 'src/auth_code_controller.dart';
export 'src/auth_controller.dart';
export 'src/auth_redirect_controller.dart';
export 'src/authorization_parser.dart';
export 'src/authorization_server.dart';
export 'src/authorizer.dart';
export 'src/exceptions.dart';
export 'src/objects.dart';
export 'src/protocols.dart';
export 'src/validator.dart';
/// A utility method to generate a password hash using the PBKDF2 scheme.
///
/// This function takes a password and salt as input and generates a secure hash
/// using the PBKDF2 (Password-Based Key Derivation Function 2) algorithm.
String generatePasswordHash(
String password,
String salt, {
int hashRounds = 1000,
int hashLength = 32,
Hash? hashFunction,
}) {
final generator = PBKDF2(hashAlgorithm: hashFunction ?? sha256);
return generator.generateBase64Key(password, salt, hashRounds, hashLength);
}
/// A utility method to generate a random base64 salt.
///
/// This function generates a random salt encoded as a base64 string.
/// The salt is useful for adding randomness to password hashing processes,
/// making them more resistant to attacks.
String generateRandomSalt({int hashLength = 32}) {
return generateAsBase64String(hashLength);
}
/// A utility method to generate a ClientID and Client Secret Pair.
///
/// This function creates an [AuthClient] instance, which can be either public or confidential,
/// depending on whether a secret is provided.
///
/// Any client that allows the authorization code flow must include [redirectURI].
///
/// Note that [secret] is hashed with a randomly generated salt, and therefore cannot be retrieved
/// later. The plain-text secret must be stored securely elsewhere.
AuthClient generateAPICredentialPair(
String clientID,
String? secret, {
String? redirectURI,
int hashLength = 32,
int hashRounds = 1000,
Hash? hashFunction,
}) {
if (secret == null) {
return AuthClient.public(clientID, redirectURI: redirectURI);
}
final salt = generateRandomSalt(hashLength: hashLength);
final hashed = generatePasswordHash(
secret,
salt,
hashRounds: hashRounds,
hashLength: hashLength,
hashFunction: hashFunction,
);
return AuthClient.withRedirectURI(clientID, hashed, salt, redirectURI);
}

View file

@ -1,489 +0,0 @@
/*
* 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 'dart:io';
import 'package:protevus_openapi/documentable.dart';
import 'package:protevus_auth/auth.dart';
import 'package:protevus_http/http.dart';
import 'package:protevus_openapi/v3.dart';
/// Provides [AuthCodeController] with application-specific behavior.
///
/// This abstract class defines the interface for a delegate that can be used
/// with [AuthCodeController] to customize the rendering of the login form.
/// It is deprecated along with [AuthCodeController], and developers are
/// advised to see the documentation for alternative approaches.
///
/// The main responsibility of this delegate is to generate an HTML
/// representation of a login form when requested by the [AuthCodeController].
@Deprecated('AuthCodeController is deprecated. See docs.')
abstract class AuthCodeControllerDelegate {
/// Returns an HTML representation of a login form.
///
/// This method is responsible for generating the HTML content of a login form
/// that will be displayed to the user when they attempt to authenticate.
///
/// The form submission should include the values of [responseType], [clientID], [state], [scope]
/// as well as user-entered username and password in `x-www-form-urlencoded` data, e.g.
///
/// POST https://example.com/auth/code
/// Content-Type: application/x-www-form-urlencoded
///
/// response_type=code&client_id=com.conduit.app&state=o9u3jla&username=bob&password=password
///
///
/// If not null, [scope] should also be included as an additional form parameter.
Future<String?> render(
AuthCodeController forController,
Uri requestUri,
String? responseType,
String clientID,
String? state,
String? scope,
);
}
/// Controller for issuing OAuth 2.0 authorization codes.
///
/// This controller handles the authorization code grant flow of OAuth 2.0. It provides
/// endpoints for both initiating the flow (GET request) and completing it (POST request).
///
/// .route("/auth/code")
/// .link(() => new AuthCodeController(authServer));
@Deprecated('Use AuthRedirectController instead.')
class AuthCodeController extends ResourceController {
/// Creates a new instance of an [AuthCodeController].
///
/// This constructor initializes an [AuthCodeController] with the provided [authServer].
/// It is marked as deprecated, and users are advised to use [AuthRedirectController] instead.
///
/// Parameters:
/// - [authServer]: The required authorization server used for handling authentication.
/// - [delegate]: An optional [AuthCodeControllerDelegate] that, if provided, allows this controller
/// to return a login page for all GET requests.
///
/// The constructor also sets the [acceptedContentTypes] to only accept
/// "application/x-www-form-urlencoded" content type.
///
/// This controller is part of the OAuth 2.0 authorization code flow and is used
/// for issuing authorization codes. However, due to its deprecated status,
/// it's recommended to transition to newer alternatives as specified in the documentation.
@Deprecated('Use AuthRedirectController instead.')
AuthCodeController(this.authServer, {this.delegate}) {
acceptedContentTypes = [
ContentType("application", "x-www-form-urlencoded")
];
}
/// A reference to the [AuthServer] used to grant authorization codes.
///
/// This [AuthServer] instance is responsible for handling the authentication
/// and authorization processes, including the generation and validation of
/// authorization codes. It is a crucial component of the OAuth 2.0 flow
/// implemented by this controller.
final AuthServer authServer;
/// A randomly generated value the client can use to verify the origin of the redirect.
///
/// Clients must include this query parameter and verify that any redirects from this
/// server have the same value for 'state' as passed in. This value is usually a randomly generated
/// session identifier.
///
/// This property is bound to the 'state' query parameter in the request URL.
/// It plays a crucial role in preventing cross-site request forgery (CSRF) attacks
/// by ensuring that the authorization request and response originate from the same client session.
///
/// The 'state' parameter should be:
/// - Unique for each authorization request
/// - Securely generated to be unguessable
/// - Stored by the client for later comparison
///
/// When the authorization server redirects the user back to the client,
/// it includes this state value, allowing the client to verify that the redirect
/// is in response to its own authorization request.
@Bind.query("state")
String? state;
/// The type of response expected from the authorization server.
///
/// This parameter is bound to the 'response_type' query parameter in the request URL.
/// For the authorization code flow, this value must be 'code'.
///
/// The response type indicates to the authorization server which grant type
/// is being utilized. In this case, 'code' signifies that the client expects
/// to receive an authorization code that can be exchanged for an access token
/// in a subsequent request.
///
/// Must be 'code'.
@Bind.query("response_type")
String? responseType;
/// The client ID of the authenticating client.
///
/// This property is bound to the 'client_id' query parameter in the request URL.
/// It represents the unique identifier of the client application requesting authorization.
///
/// The client ID must be registered and valid according to the [authServer].
/// It is used to identify the client during the OAuth 2.0 authorization process.
///
/// This field is nullable, but typically required for most OAuth 2.0 flows.
/// If not provided or invalid, the authorization request may be rejected.
@Bind.query("client_id")
String? clientID;
/// Renders an HTML login form.
final AuthCodeControllerDelegate? delegate;
/// Returns an HTML login form for OAuth 2.0 authorization.
///
/// A client that wishes to authenticate with this server should direct the user
/// to this page. The user will enter their username and password that is sent as a POST
/// request to this same controller.
///
/// The 'client_id' must be a registered, valid client of this server. The client must also provide
/// a [state] to this request and verify that the redirect contains the same value in its query string.
@Operation.get()
Future<Response> getAuthorizationPage({
/// A space-delimited list of access scopes to be requested by the form submission on the returned page.
@Bind.query("scope") String? scope,
}) async {
if (clientID == null) {
return Response.badRequest();
}
if (delegate == null) {
return Response(405, {}, null);
}
final renderedPage = await delegate!
.render(this, request!.raw.uri, responseType, clientID!, state, scope);
return Response.ok(renderedPage)..contentType = ContentType.html;
}
/// Creates a one-time use authorization code.
///
/// This method handles the POST request for the OAuth 2.0 authorization code grant flow.
/// It authenticates the user with the provided credentials and, if successful, generates
/// a one-time use authorization code.
///
/// This method is typically invoked by the login form returned from the GET to this controller.
@Operation.post()
Future<Response> authorize({
/// The username of the authenticating user.
///
/// This parameter is bound to the 'username' query parameter in the request URL.
/// It represents the username of the user attempting to authenticate.
///
/// The username is used in conjunction with the password to verify the user's identity
/// during the OAuth 2.0 authorization code grant flow. It is a crucial part of the
/// user authentication process.
///
/// This field is nullable, but typically required for successful authentication.
/// If not provided or invalid, the authorization request may be rejected.
@Bind.query("username") String? username,
/// The password of the authenticating user.
///
/// This parameter is bound to the 'password' query parameter in the request URL.
/// It represents the password of the user attempting to authenticate.
///
/// The password is used in conjunction with the username to verify the user's identity
/// during the OAuth 2.0 authorization code grant flow. It is a crucial part of the
/// user authentication process.
///
/// This field is nullable, but typically required for successful authentication.
/// If not provided or invalid, the authorization request may be rejected.
///
/// Note: Transmitting passwords as query parameters is not recommended for production
/// environments due to security concerns. This approach should only be used in
/// controlled, secure environments or for testing purposes.
@Bind.query("password") String? password,
/// A space-delimited list of access scopes being requested.
///
/// This parameter is bound to the 'scope' query parameter in the request URL.
/// It represents the permissions that the client is requesting access to.
///
/// The scope is typically a string containing one or more space-separated
/// scope values. Each scope value represents a specific permission or
/// set of permissions that the client is requesting.
///
/// For example, a scope might look like: "read_profile edit_profile"
///
/// The authorization server can use this information to present
/// the user with a consent screen, allowing them to approve or deny
/// specific permissions requested by the client.
///
/// This field is optional. If not provided, the authorization server
/// may assign a default set of scopes or handle the request according
/// to its own policies.
@Bind.query("scope") String? scope,
}) async {
final client = await authServer.getClient(clientID!);
if (state == null) {
return _redirectResponse(
null,
null,
error: AuthServerException(AuthRequestError.invalidRequest, client),
);
}
if (responseType != "code") {
if (client?.redirectURI == null) {
return Response.badRequest();
}
return _redirectResponse(
null,
state,
error: AuthServerException(AuthRequestError.invalidRequest, client),
);
}
try {
final scopes = scope?.split(" ").map((s) => AuthScope(s)).toList();
final authCode = await authServer.authenticateForCode(
username,
password,
clientID!,
requestedScopes: scopes,
);
return _redirectResponse(client!.redirectURI, state, code: authCode.code);
} on FormatException {
return _redirectResponse(
null,
state,
error: AuthServerException(AuthRequestError.invalidScope, client),
);
} on AuthServerException catch (e) {
return _redirectResponse(null, state, error: e);
}
}
/// Overrides the default documentation for the request body of this controller's operations.
///
/// This method is called during the OpenAPI documentation generation process.
/// It modifies the request body schema for POST operations to:
/// 1. Set the format of the 'password' field to "password".
/// 2. Mark certain fields as required in the request body.
///
/// The method specifically targets the "application/x-www-form-urlencoded" content type
/// in POST requests. It updates the schema to indicate that the 'password' field should
/// be treated as a password input, and sets the following fields as required:
/// - client_id
/// - state
/// - response_type
/// - username
/// - password
///
/// Returns:
/// An [APIRequestBody] object representing the documented request body,
/// or null if there is no request body for the operation.
@override
APIRequestBody? documentOperationRequestBody(
APIDocumentContext context,
Operation? operation,
) {
final body = super.documentOperationRequestBody(context, operation);
if (operation!.method == "POST") {
body!.content!["application/x-www-form-urlencoded"]!.schema!
.properties!["password"]!.format = "password";
body.content!["application/x-www-form-urlencoded"]!.schema!.isRequired = [
"client_id",
"state",
"response_type",
"username",
"password"
];
}
return body;
}
/// Overrides the default documentation for operation parameters.
///
/// This method is called during the OpenAPI documentation generation process.
/// It modifies the parameter documentation for the controller's operations by:
/// 1. Retrieving the default parameters using the superclass method.
/// 2. Setting all parameters except 'scope' as required.
///
/// Parameters:
/// context: The [APIDocumentContext] for the current documentation generation.
/// operation: The [Operation] being documented.
///
/// Returns:
/// A List of [APIParameter] objects representing the documented parameters,
/// with updated 'isRequired' properties.
@override
List<APIParameter> documentOperationParameters(
APIDocumentContext context,
Operation? operation,
) {
final params = super.documentOperationParameters(context, operation)!;
params.where((p) => p.name != "scope").forEach((p) {
p.isRequired = true;
});
return params;
}
/// Generates documentation for the operation responses of this controller.
///
/// This method overrides the default behavior to provide custom documentation
/// for the GET and POST operations of the AuthCodeController.
///
/// For GET requests:
/// - Defines a 200 OK response that serves a login form in HTML format.
///
/// For POST requests:
/// - Defines a 302 Found (Moved Temporarily) response for successful requests,
/// indicating that the 'code' query parameter in the redirect URI contains
/// the authorization code, or an 'error' parameter is present for errors.
/// - Defines a 400 Bad Request response for cases where the 'client_id' is
/// invalid and the redirect URI cannot be verified.
///
/// Parameters:
/// context: The API documentation context.
/// operation: The operation being documented.
///
/// Returns:
/// A Map of status codes to APIResponse objects describing the possible
/// responses for the operation.
@override
Map<String, APIResponse> documentOperationResponses(
APIDocumentContext context,
Operation? operation,
) {
if (operation!.method == "GET") {
return {
"200": APIResponse.schema(
"Serves a login form.",
APISchemaObject.string(),
contentTypes: ["text/html"],
)
};
} else if (operation.method == "POST") {
return {
"${HttpStatus.movedTemporarily}": APIResponse(
"If successful, the query parameter of the redirect URI named 'code' contains authorization code. "
"Otherwise, the query parameter 'error' is present and contains a error string.",
headers: {
"Location": APIHeader()
..schema = APISchemaObject.string(format: "uri")
},
),
"${HttpStatus.badRequest}": APIResponse.schema(
"If 'client_id' is invalid, the redirect URI cannot be verified and this response is sent.",
APISchemaObject.object({"error": APISchemaObject.string()}),
contentTypes: ["application/json"],
)
};
}
throw StateError("AuthCodeController documentation failed.");
}
/// Customizes the documentation for the operations of this controller.
///
/// This method overrides the default implementation to add additional
/// information specific to the OAuth 2.0 authorization code flow.
///
/// It performs the following tasks:
/// 1. Calls the superclass method to get the default operation documentation.
/// 2. Updates the authorization URL in the documented authorization code flow
/// of the auth server to match the current route.
///
/// Parameters:
/// context: The [APIDocumentContext] for the current documentation generation.
/// route: The route string for this controller.
/// path: The [APIPath] object representing the path in the API documentation.
///
/// Returns:
/// A Map of operation names to [APIOperation] objects representing the
/// documented operations for this controller.
@override
Map<String, APIOperation> documentOperations(
APIDocumentContext context,
String route,
APIPath path,
) {
final ops = super.documentOperations(context, route, path);
authServer.documentedAuthorizationCodeFlow.authorizationURL =
Uri(path: route.substring(1));
return ops;
}
/// Generates a redirect response for the OAuth 2.0 authorization code flow.
///
/// This method constructs a redirect URI based on the provided parameters and
/// returns a Response object with appropriate headers for redirection.
///
/// Parameters:
/// - [inputUri]: The base URI to redirect to. If null, falls back to the client's redirectURI.
/// - [clientStateOrNull]: The state parameter provided by the client. If not null, it's included in the redirect URI.
/// - [code]: The authorization code to be included in the redirect URI. Optional.
/// - [error]: An AuthServerException containing error details. Optional.
///
/// Returns:
/// - A Response object with status 302 (Found) and appropriate headers for redirection.
/// - If no valid redirect URI can be constructed, returns a 400 (Bad Request) response.
///
/// The method constructs the redirect URI by:
/// 1. Determining the base URI (from input or client's redirect URI)
/// 2. Adding query parameters for code, state, and error as applicable
/// 3. Constructing a new URI with these parameters
///
/// The response includes headers for location, cache control, and pragma.
static Response _redirectResponse(
String? inputUri,
String? clientStateOrNull, {
String? code,
AuthServerException? error,
}) {
final uriString = inputUri ?? error!.client?.redirectURI;
if (uriString == null) {
return Response.badRequest(body: {"error": error!.reasonString});
}
final redirectURI = Uri.parse(uriString);
final queryParameters =
Map<String, String?>.from(redirectURI.queryParameters);
if (code != null) {
queryParameters["code"] = code;
}
if (clientStateOrNull != null) {
queryParameters["state"] = clientStateOrNull;
}
if (error != null) {
queryParameters["error"] = error.reasonString;
}
final responseURI = Uri(
scheme: redirectURI.scheme,
userInfo: redirectURI.userInfo,
host: redirectURI.host,
port: redirectURI.port,
path: redirectURI.path,
queryParameters: queryParameters,
);
return Response(
HttpStatus.movedTemporarily,
{
HttpHeaders.locationHeader: responseURI.toString(),
HttpHeaders.cacheControlHeader: "no-store",
HttpHeaders.pragmaHeader: "no-cache"
},
null,
);
}
}

View file

@ -1,459 +0,0 @@
/*
* 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 'dart:io';
import 'package:protevus_openapi/documentable.dart';
import 'package:protevus_auth/auth.dart';
import 'package:protevus_http/http.dart';
import 'package:protevus_openapi/v3.dart';
/// Controller for issuing and refreshing OAuth 2.0 access tokens.
///
/// This controller issues and refreshes access tokens. Access tokens are issued for valid username and password (resource owner password grant)
/// or for an authorization code (authorization code grant) from a [AuthRedirectController].
///
/// See operation method [grant] for more details.
///
/// Usage:
///
/// router
/// .route("/auth/token")
/// .link(() => new AuthController(authServer));
///
class AuthController extends ResourceController {
/// Creates a new instance of an [AuthController].
///
/// [authServer] is the isRequired authorization server that grants tokens.
AuthController(this.authServer) {
acceptedContentTypes = [
ContentType("application", "x-www-form-urlencoded")
];
}
/// A reference to the [AuthServer] this controller uses to grant tokens.
final AuthServer authServer;
/// Required basic authentication Authorization header containing client ID and secret for the authenticating client.
///
/// Requests must contain the client ID and client secret in the authorization header,
/// using the basic authentication scheme. If the client is a public client - i.e., no client secret -
/// the client secret is omitted from the Authorization header.
///
/// Example: com.stablekernel.public is a public client. The Authorization header should be constructed
/// as so:
///
/// Authorization: Basic base64("com.stablekernel.public:")
///
/// Notice the trailing colon indicates that the client secret is the empty string.
@Bind.header(HttpHeaders.authorizationHeader)
String? authHeader;
final AuthorizationBasicParser _parser = const AuthorizationBasicParser();
/// This class, AuthController, is responsible for handling OAuth 2.0 token operations.
/// It provides functionality for issuing and refreshing access tokens using various grant types.
///
/// Key features:
/// - Supports 'password', 'refresh_token', and 'authorization_code' grant types
/// - Handles client authentication via Basic Authorization header
/// - Processes token requests and returns RFC6749 compliant responses
/// - Includes error handling for various authentication scenarios
/// - Provides OpenAPI documentation support
///
/// The main method, 'grant', processes token requests based on the provided grant type.
/// It interacts with an AuthServer to perform the actual authentication and token generation.
///
/// This controller also includes methods for generating API documentation,
/// including operation parameters, request body, and responses.
///
/// Usage:
/// router
/// .route("/auth/token")
/// .link(() => new AuthController(authServer));
///
/// Note: This controller expects client credentials to be provided in the Authorization header
/// using the Basic authentication scheme.
///
/// Creates or refreshes an authentication token.
///
/// When grant_type is 'password', there must be username and password values.
/// When grant_type is 'refresh_token', there must be a refresh_token value.
/// When grant_type is 'authorization_code', there must be a authorization_code value.
///
/// This endpoint requires client_id authentication. The Authorization header must
/// include a valid Client ID and Secret in the Basic authorization scheme format.
@Operation.post()
Future<Response> grant({
/// The username of the user attempting to authenticate.
///
/// This parameter is typically used with the 'password' grant type.
/// It should be provided as a query parameter in the request.
@Bind.query("username") String? username,
/// The password of the user attempting to authenticate.
///
/// This parameter is typically used with the 'password' grant type.
/// It should be provided as a query parameter in the request.
/// Note: Sending passwords as query parameters is not recommended for production environments due to security concerns.
@Bind.query("password") String? password,
/// The refresh token used to obtain a new access token.
///
/// This parameter is typically used with the 'refresh_token' grant type.
/// It should be provided as a query parameter in the request.
/// The refresh token is used to request a new access token when the current one has expired.
///
/// Example:
/// curl -X POST -d "grant_type=refresh_token&refresh_token=<refresh_token>" https://example.com/auth/token
///
/// Note: The refresh token should be securely stored and managed by the client application.
/// It is important to handle refresh tokens with care to prevent unauthorized access to user resources.
///
/// See also:
/// - [RFC 6749, Section 6](https://tools.ietf.org/html/rfc6749#section-6) for more details on the refresh token grant type.
/// - [OAuth 2.0 Refresh Token Grant](https://oauth.net/2/grant-types/refresh-token/) for a detailed explanation of the refresh token grant type.
/// - [OAuth 2.0 Security Best Current Practice](https://tools.ietf.org/html/draft-ietf-oauth-security-topics-13#section-2.1) for security considerations when implementing OAuth 2.0.
@Bind.query("refresh_token") String? refreshToken,
/// The authorization code obtained from the authorization server.
///
/// This parameter is typically used with the 'authorization_code' grant type.
/// It should be provided as a query parameter in the request.
/// The authorization code is used to request an access token after the user has granted permission to the client application.
///
/// Example:
/// curl -X POST -d "grant_type=authorization_code&code=<authorization_code>&redirect_uri=<redirect_uri>" https://example.com/auth/token
///
/// Note: The authorization code should be securely transmitted and used only once to prevent replay attacks.
/// It is important to handle authorization codes with care to protect user data and ensure the security of the OAuth 2.0 flow.
///
/// See also:
/// - [RFC 6749, Section 4.1.3](https://tools.ietf.org/html/rfc6749#section-4.1.3) for more details on the authorization code grant type.
/// - [OAuth 2.0 Authorization Code Grant](https://oauth.net/2/grant-types/authorization-code/) for a detailed explanation of the authorization code grant type.
/// - [OAuth 2.0 Security Best Current Practice](https://tools.ietf.org/html/draft-ietf-oauth-security-topics-13#section-2.1) for security considerations when implementing OAuth 2.0.
@Bind.query("code") String? authCode,
/// The URI to which the authorization server will redirect the user-agent after obtaining authorization.
///
/// This parameter is typically used with the 'authorization_code' grant type.
/// It should be provided as a query parameter in the request.
/// The redirect URI is used to ensure that the authorization code is sent to the correct client application.
///
/// Example:
/// curl -X POST -d "grant_type=authorization_code&code=<authorization_code>&redirect_uri=https://example.com/callback" https://example.com/auth/token
///
/// Note: The redirect URI should be registered with the authorization server and should match the URI used during the authorization request.
/// It is important to handle redirect URIs with care to prevent unauthorized access to user resources.
///
/// See also:
/// - [RFC 6749, Section 4.1.3](https://tools.ietf.org/html/rfc6749#section-4.1.3) for more details on the authorization code grant type.
/// - [OAuth 2.0 Authorization Code Grant](https://oauth.net/2/grant-types/authorization-code/) for a detailed explanation of the authorization code grant type.
/// - [OAuth 2.0 Security Best Current Practice](https://tools.ietf.org/html/draft-ietf-oauth-security-topics-13#section-2.1) for security considerations when implementing OAuth 2.0.
@Bind.query("grant_type") String? grantType,
/// The scope of the access request, which defines the resources and permissions that the client application is requesting.
///
/// This parameter is optional and should be provided as a query parameter in the request.
/// The scope value is a space-delimited list of scope identifiers, which indicate the specific resources and permissions that the client application needs to access.
///
/// Example:
/// curl -X POST -d "grant_type=authorization_code&code=<authorization_code>&redirect_uri=<redirect_uri>&scope=read write" https://example.com/auth/token
///
/// Note: The scope parameter should be used to limit the access granted to the client application to only the necessary resources and permissions.
/// It is important to handle scope values with care to ensure that the client application does not have unintended access to user resources.
///
/// See also:
/// - [RFC 6749, Section 3.3](https://tools.ietf.org/html/rfc6749#section-3.3) for more details on the scope parameter.
/// - [OAuth 2.0 Security Best Current Practice](https://tools.ietf.org/html/draft-ietf-oauth-security-topics-13#section-2.2) for security considerations when implementing OAuth 2.0 scope.
/// - [OAuth 2.0 Scope](https://oauth.net/2/scope/) for a detailed explanation of the scope parameter and its usage.
@Bind.query("scope") String? scope,
}) async {
AuthBasicCredentials basicRecord;
try {
basicRecord = _parser.parse(authHeader);
} on AuthorizationParserException {
return _responseForError(AuthRequestError.invalidClient);
}
try {
final scopes = scope?.split(" ").map((s) => AuthScope(s)).toList();
if (grantType == "password") {
final token = await authServer.authenticate(
username,
password,
basicRecord.username,
basicRecord.password,
requestedScopes: scopes,
);
return AuthController.tokenResponse(token);
} else if (grantType == "refresh_token") {
final token = await authServer.refresh(
refreshToken,
basicRecord.username,
basicRecord.password,
requestedScopes: scopes,
);
return AuthController.tokenResponse(token);
} else if (grantType == "authorization_code") {
if (scope != null) {
return _responseForError(AuthRequestError.invalidRequest);
}
final token = await authServer.exchange(
authCode, basicRecord.username, basicRecord.password);
return AuthController.tokenResponse(token);
} else if (grantType == null) {
return _responseForError(AuthRequestError.invalidRequest);
}
} on FormatException {
return _responseForError(AuthRequestError.invalidScope);
} on AuthServerException catch (e) {
return _responseForError(e.reason);
}
return _responseForError(AuthRequestError.unsupportedGrantType);
}
/// Transforms a [AuthToken] into a [Response] object with an RFC6749 compliant JSON token
/// as the HTTP response body.
///
/// This static method takes an [AuthToken] object and converts it into a [Response] object
/// that adheres to the OAuth 2.0 specification (RFC6749). The response includes:
/// - A status code of 200 (OK)
/// - Headers to prevent caching of the token
/// - A body containing the token information in JSON format
///
/// Parameters:
/// - token: An [AuthToken] object containing the authentication token details
///
/// Returns:
/// A [Response] object with the token information, ready to be sent to the client
///
/// Example usage:
/// ```dart
/// AuthToken myToken = // ... obtain token
/// Response response = AuthController.tokenResponse(myToken);
/// ```
///
/// See also:
/// - [RFC6749](https://tools.ietf.org/html/rfc6749) for the OAuth 2.0 specification
/// - [AuthToken] for the structure of the token object
static Response tokenResponse(AuthToken token) {
return Response(
HttpStatus.ok,
{"Cache-Control": "no-store", "Pragma": "no-cache"},
token.asMap(),
);
}
/// Processes the response before it is sent, specifically handling duplicate parameter errors.
///
/// This method is called just before a response is sent. It checks for responses with a 400 status code
/// and modifies the error message in case of duplicate parameters in the request, which violates the OAuth 2.0 specification.
///
/// The method performs the following actions:
/// 1. Checks if the response status code is 400 (Bad Request).
/// 2. If the response body contains an "error" key with a string value, it examines the error message.
/// 3. If the error message indicates multiple values (likely due to duplicate parameters), it replaces the error message
/// with a standard "invalid_request" error as defined in the OAuth 2.0 specification.
///
/// This post-processing helps to maintain compliance with the OAuth 2.0 specification by providing a standard error
/// response for invalid requests, even in the case of duplicate parameters which are not explicitly handled elsewhere.
///
/// Parameters:
/// response: The Response object that will be sent to the client.
///
/// Note: This method directly modifies the response object if conditions are met.
@override
void willSendResponse(Response response) {
if (response.statusCode == 400) {
// This post-processes the response in the case that duplicate parameters
// were in the request, which violates oauth2 spec. It just adjusts the error message.
// This could be hardened some.
final body = response.body;
if (body != null && body["error"] is String) {
final errorMessage = body["error"] as String;
if (errorMessage.startsWith("multiple values")) {
response.body = {
"error":
AuthServerException.errorString(AuthRequestError.invalidRequest)
};
}
}
}
}
/// Modifies the list of API parameters documented for this operation.
///
/// This method overrides the default behavior to remove the 'Authorization' header
/// from the list of documented parameters. This is typically done because the
/// Authorization header is handled separately in OAuth 2.0 flows and doesn't need
/// to be explicitly documented as an operation parameter.
///
/// Parameters:
/// - context: The current API documentation context.
/// - operation: The operation being documented (can be null).
///
/// Returns:
/// A list of [APIParameter] objects representing the documented parameters
/// for this operation, with the Authorization header removed.
@override
List<APIParameter> documentOperationParameters(
APIDocumentContext context,
Operation? operation,
) {
final parameters = super.documentOperationParameters(context, operation)!;
parameters.removeWhere((p) => p.name == HttpHeaders.authorizationHeader);
return parameters;
}
/// Customizes the documentation for the request body of this operation.
///
/// This method overrides the default behavior to add specific requirements
/// and formatting for the OAuth 2.0 token endpoint:
///
/// 1. It marks the 'grant_type' parameter as required in the request body.
/// 2. It sets the format of the 'password' field to "password", indicating
/// that it should be treated as a sensitive input in API documentation tools.
///
/// Parameters:
/// - context: The current API documentation context.
/// - operation: The operation being documented (can be null).
///
/// Returns:
/// An [APIRequestBody] object with the customized schema for the request body.
@override
APIRequestBody documentOperationRequestBody(
APIDocumentContext context,
Operation? operation,
) {
final body = super.documentOperationRequestBody(context, operation)!;
body.content!["application/x-www-form-urlencoded"]!.schema!.isRequired = [
"grant_type"
];
body.content!["application/x-www-form-urlencoded"]!.schema!
.properties!["password"]!.format = "password";
return body;
}
/// Customizes the API documentation for the operations handled by this controller.
///
/// This method overrides the default behavior to:
/// 1. Add OAuth 2.0 client authentication security requirement to all operations.
/// 2. Set the token and refresh URLs for the documented authorization code flow.
/// 3. Set the token and refresh URLs for the documented password flow.
///
/// Parameters:
/// - context: The current API documentation context.
/// - route: The route string for the current operations.
/// - path: The APIPath object representing the current path.
///
/// Returns:
/// A map of operation names to APIOperation objects with the customized documentation.
@override
Map<String, APIOperation> documentOperations(
APIDocumentContext context,
String route,
APIPath path,
) {
final operations = super.documentOperations(context, route, path);
operations.forEach((_, op) {
op.security = [
APISecurityRequirement({"oauth2-client-authentication": []})
];
});
final relativeUri = Uri(path: route.substring(1));
authServer.documentedAuthorizationCodeFlow.tokenURL = relativeUri;
authServer.documentedAuthorizationCodeFlow.refreshURL = relativeUri;
authServer.documentedPasswordFlow.tokenURL = relativeUri;
authServer.documentedPasswordFlow.refreshURL = relativeUri;
return operations;
}
/// Defines the API responses for the token operation in OpenAPI documentation.
///
/// This method overrides the default behavior to provide custom documentation
/// for the responses of the token endpoint. It describes two possible responses:
///
/// 1. A successful response (200 OK) when credentials are successfully exchanged for a token.
/// This response includes details about the issued token such as access_token, token_type,
/// expiration time, refresh_token, and scope.
///
/// 2. An error response (400 Bad Request) for cases of invalid credentials or missing parameters.
/// This response includes an error message.
///
/// Parameters:
/// - context: The current API documentation context.
/// - operation: The operation being documented (can be null).
///
/// Returns:
/// A map of status codes to APIResponse objects describing the possible
/// responses for this operation.
@override
Map<String, APIResponse> documentOperationResponses(
APIDocumentContext context,
Operation? operation,
) {
return {
"200": APIResponse.schema(
"Successfully exchanged credentials for token",
APISchemaObject.object({
"access_token": APISchemaObject.string(),
"token_type": APISchemaObject.string(),
"expires_in": APISchemaObject.integer(),
"refresh_token": APISchemaObject.string(),
"scope": APISchemaObject.string()
}),
contentTypes: ["application/json"],
),
"400": APIResponse.schema(
"Invalid credentials or missing parameters.",
APISchemaObject.object({"error": APISchemaObject.string()}),
contentTypes: ["application/json"],
)
};
}
/// Creates a Response object for an authentication error.
///
/// This method generates a standardized HTTP response for various authentication
/// errors that may occur during the OAuth 2.0 flow. It uses the [AuthRequestError]
/// enum to determine the specific error and creates a response with:
/// - A status code of 400 (Bad Request)
/// - A JSON body containing an "error" key with a description of the error
///
/// Parameters:
/// - error: An [AuthRequestError] enum representing the specific authentication error.
///
/// Returns:
/// A [Response] object with status code 400 and a JSON body describing the error.
///
/// Example usage:
/// ```dart
/// Response errorResponse = _responseForError(AuthRequestError.invalidRequest);
/// ```
///
/// The error string in the response body is generated using [AuthServerException.errorString],
/// ensuring consistency with OAuth 2.0 error reporting standards.
Response _responseForError(AuthRequestError error) {
return Response.badRequest(
body: {"error": AuthServerException.errorString(error)},
);
}
}

View file

@ -1,587 +0,0 @@
/*
* 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 'dart:io';
import 'package:protevus_openapi/documentable.dart';
import 'package:protevus_auth/auth.dart';
import 'package:protevus_http/http.dart';
import 'package:protevus_openapi/v3.dart';
/// Abstract class defining the interface for providing application-specific behavior to [AuthRedirectController].
///
/// This delegate is responsible for rendering the HTML login form when [AuthRedirectController.getAuthorizationPage]
/// is called in response to a GET request. Implementations of this class should customize the login form
/// according to the application's needs while ensuring that the form submission adheres to the required format.
///
/// The rendered form should:
/// - Be submitted as a POST request to the [requestUri].
/// - Include all provided parameters (responseType, clientID, state, scope) in the form submission.
/// - Collect and include user-entered username and password.
/// - Use 'application/x-www-form-urlencoded' as the Content-Type for form submission.
///
/// Example of expected form submission:
///
/// POST https://example.com/auth/code
/// Content-Type: application/x-www-form-urlencoded
///
/// response_type=code&client_id=com.conduit.app&state=o9u3jla&username=bob&password=password
///
/// Implementations should take care to handle all provided parameters and ensure secure transmission of credentials.
abstract class AuthRedirectControllerDelegate {
/// Returns an HTML representation of a login form.
///
/// This method is responsible for generating and returning the HTML content for a login form
/// when [AuthRedirectController.getAuthorizationPage] is called in response to a GET request.
///
/// The form submission should include the values of [responseType], [clientID], [state], [scope]
/// as well as user-entered username and password in `x-www-form-urlencoded` data, e.g.
///
/// POST https://example.com/auth/code
/// Content-Type: application/x-www-form-urlencoded
///
/// response_type=code&client_id=com.conduit.app&state=o9u3jla&username=bob&password=password
///
///
/// If not null, [scope] should also be included as an additional form parameter.
Future<String?> render(
AuthRedirectController forController,
Uri requestUri,
String? responseType,
String clientID,
String? state,
String? scope,
);
}
/// Controller for issuing OAuth 2.0 authorization codes and tokens.
///
/// This controller provides an endpoint for creating an OAuth 2.0 authorization code or access token. An authorization code
/// can be exchanged for an access token with an [AuthController]. This is known as the OAuth 2.0 'Authorization Code Grant' flow.
/// Returning an access token is known as the OAuth 2.0 'Implicit Grant' flow.
///
/// See operation methods [getAuthorizationPage] and [authorize] for more details.
///
/// Usage:
///
/// router
/// .route("/auth/code")
/// .link(() => new AuthRedirectController(authServer));
///
class AuthRedirectController extends ResourceController {
/// Creates a new instance of an [AuthRedirectController].
///
/// This constructor initializes an [AuthRedirectController] with the provided [authServer].
///
/// Parameters:
/// - [authServer]: The required authorization server.
/// - [delegate]: Optional. If provided, this controller will return a login page for all GET requests.
/// - [allowsImplicit]: Optional. Defaults to true. Determines if the controller allows the Implicit Grant Flow.
///
/// The constructor also sets the [acceptedContentTypes] to ["application/x-www-form-urlencoded"].
///
/// Usage:
/// ```dart
/// final authRedirectController = AuthRedirectController(
/// myAuthServer,
/// delegate: myDelegate,
/// allowsImplicit: false,
/// );
/// ```
AuthRedirectController(
this.authServer, {
this.delegate,
this.allowsImplicit = true,
}) {
acceptedContentTypes = [
ContentType("application", "x-www-form-urlencoded")
];
}
/// A pre-defined Response object for unsupported response types.
///
/// This static final variable creates a Response object with:
/// - HTTP status code 400 (Bad Request)
/// - HTML content indicating an "unsupported_response_type" error
/// - Content-Type set to text/html
///
/// This response is used when the 'response_type' parameter in the request
/// is neither 'code' nor 'token', or when 'token' is requested but implicit
/// grant flow is not allowed.
static final Response _unsupportedResponseTypeResponse = Response.badRequest(
body: "<h1>Error</h1><p>unsupported_response_type</p>",
)..contentType = ContentType.html;
/// A reference to the [AuthServer] used to grant authorization codes and access tokens.
///
/// This property holds an instance of [AuthServer] which is responsible for
/// handling the authentication and authorization processes. It is used by
/// this controller to issue authorization codes and access tokens as part of
/// the OAuth 2.0 flow.
///
/// The [AuthServer] instance should be properly configured and initialized
/// before being assigned to this property.
late final AuthServer authServer;
/// Determines whether the controller allows the OAuth 2.0 Implicit Grant Flow.
///
/// When set to true, the controller will process requests for access tokens
/// directly (response_type=token). When false, such requests will be rejected.
///
/// This property is typically set in the constructor and should not be
/// modified after initialization.
final bool allowsImplicit;
/// A randomly generated value the client can use to verify the origin of the redirect.
///
/// This property is bound to the 'state' query parameter of the incoming request.
/// It serves as a security measure to prevent cross-site request forgery (CSRF) attacks.
///
/// Clients must include this query parameter when initiating an authorization request.
/// Upon receiving a redirect from this server, clients should verify that the 'state'
/// value in the redirect matches the one they initially sent. This ensures that the
/// response is for the request they initiated and not for a malicious request.
///
/// The value of 'state' is typically a randomly generated string or session identifier.
/// It should be unique for each authorization request to maintain security.
///
/// Example usage:
/// ```
/// GET /authorize?response_type=code&client_id=CLIENT_ID&state=RANDOM_STATE
/// ```
@Bind.query("state")
String? state;
/// The type of response requested from the authorization endpoint.
///
/// This property is bound to the 'response_type' query parameter of the incoming request.
/// It must be either 'code' or 'token':
/// - 'code': Indicates that the client is initiating the authorization code flow.
/// - 'token': Indicates that the client is initiating the implicit flow.
///
/// The value of this property determines the type of credential (authorization code or access token)
/// that will be issued upon successful authentication.
///
/// Note: The availability of the 'token' response type depends on the [allowsImplicit] setting
/// of the controller.
@Bind.query("response_type")
String? responseType;
/// The client ID of the authenticating client.
///
/// This property is bound to the 'client_id' query parameter of the incoming request.
/// It represents the unique identifier of the client application requesting authorization.
///
/// The client ID must be registered and valid according to the [authServer].
/// It is used to identify the client during the authorization process and to ensure
/// that only authorized clients can request access tokens or authorization codes.
///
/// This field is nullable, but typically required for most OAuth 2.0 flows.
/// If not provided in the request, it may lead to authorization failures.
///
/// Example usage in a request URL:
/// ```
/// GET /authorize?client_id=my_client_id&...
/// ```
@Bind.query("client_id")
String? clientID;
/// Delegate responsible for rendering the HTML login form.
///
/// If provided, this delegate will be used to generate a custom login page
/// when [getAuthorizationPage] is called. The delegate's [render] method
/// is responsible for creating the HTML content of the login form.
///
/// When this property is null, the controller will not serve a login page
/// and will respond with a 405 Method Not Allowed status for GET requests.
///
/// This delegate allows for customization of the login experience while
/// maintaining the required OAuth 2.0 authorization flow.
final AuthRedirectControllerDelegate? delegate;
/// Returns an HTML login form for OAuth 2.0 authorization.
///
/// A client that wishes to authenticate with this server should direct the user
/// to this page. The user will enter their username and password that is sent as a POST
/// request to this same controller.
///
/// The 'client_id' must be a registered, valid client of this server. The client must also provide
/// a [state] to this request and verify that the redirect contains the same value in its query string.
@Operation.get()
Future<Response> getAuthorizationPage({
/// A space-delimited list of access scopes to be requested by the form submission on the returned page.
@Bind.query("scope") String? scope,
}) async {
if (delegate == null) {
return Response(405, {}, null);
}
if (responseType != "code" && responseType != "token") {
return _unsupportedResponseTypeResponse;
}
if (responseType == "token" && !allowsImplicit) {
return _unsupportedResponseTypeResponse;
}
final renderedPage = await delegate!
.render(this, request!.raw.uri, responseType, clientID!, state, scope);
if (renderedPage == null) {
return Response.notFound();
}
return Response.ok(renderedPage)..contentType = ContentType.html;
}
/// Creates a one-time use authorization code or an access token.
///
/// This method handles the OAuth 2.0 authorization process, responding with a redirect
/// that contains either an authorization code ('code') or an access token ('token')
/// along with the passed in 'state'. If the request fails, the redirect URL will
/// contain an 'error' instead of the authorization code or access token.
///
/// This method is typically invoked by the login form returned from the GET to this controller.
@Operation.post()
Future<Response> authorize({
/// The username of the authenticating user.
@Bind.query("username") String? username,
/// The password of the authenticating user.
@Bind.query("password") String? password,
/// A space-delimited list of access scopes being requested.
@Bind.query("scope") String? scope,
}) async {
if (clientID == null) {
return Response.badRequest();
}
final client = await authServer.getClient(clientID!);
if (client?.redirectURI == null) {
return Response.badRequest();
}
if (responseType == "token" && !allowsImplicit) {
return _unsupportedResponseTypeResponse;
}
if (state == null) {
return _redirectResponse(
null,
null,
error: AuthServerException(AuthRequestError.invalidRequest, client),
);
}
try {
final scopes = scope?.split(" ").map((s) => AuthScope(s)).toList();
if (responseType == "code") {
if (client!.hashedSecret == null) {
return _redirectResponse(
null,
state,
error: AuthServerException(
AuthRequestError.unauthorizedClient,
client,
),
);
}
final authCode = await authServer.authenticateForCode(
username,
password,
clientID!,
requestedScopes: scopes,
);
return _redirectResponse(
client.redirectURI,
state,
code: authCode.code,
);
} else if (responseType == "token") {
final token = await authServer.authenticate(
username,
password,
clientID!,
null,
requestedScopes: scopes,
);
return _redirectResponse(client!.redirectURI, state, token: token);
} else {
return _redirectResponse(
null,
state,
error: AuthServerException(AuthRequestError.invalidRequest, client),
);
}
} on FormatException {
return _redirectResponse(
null,
state,
error: AuthServerException(AuthRequestError.invalidScope, client),
);
} on AuthServerException catch (e) {
if (responseType == "token" &&
e.reason == AuthRequestError.invalidGrant) {
return _redirectResponse(
null,
state,
error: AuthServerException(AuthRequestError.accessDenied, client),
);
}
return _redirectResponse(null, state, error: e);
}
}
/// Customizes the API documentation for the request body of this controller's operations.
///
/// This method overrides the default implementation to add specific details to the
/// POST operation's request body schema:
/// - Sets the format of the 'password' field to "password".
/// - Marks 'client_id', 'state', 'response_type', 'username', and 'password' as required fields.
///
/// Parameters:
/// - context: The API documentation context.
/// - operation: The operation being documented.
///
/// Returns:
/// The modified [APIRequestBody] object, or null if no modifications were made.
@override
APIRequestBody? documentOperationRequestBody(
APIDocumentContext context,
Operation? operation,
) {
final body = super.documentOperationRequestBody(context, operation);
if (operation!.method == "POST") {
body!.content!["application/x-www-form-urlencoded"]!.schema!
.properties!["password"]!.format = "password";
body.content!["application/x-www-form-urlencoded"]!.schema!.isRequired = [
"client_id",
"state",
"response_type",
"username",
"password"
];
}
return body;
}
/// Customizes the API documentation for the operation parameters of this controller.
///
/// This method overrides the default implementation to mark all parameters
/// as required, except for the 'scope' parameter. It does this by:
/// 1. Calling the superclass method to get the initial list of parameters.
/// 2. Iterating through all parameters except 'scope'.
/// 3. Setting the 'isRequired' property of each parameter to true.
///
/// Parameters:
/// - context: The API documentation context.
/// - operation: The operation being documented.
///
/// Returns:
/// A list of [APIParameter] objects with updated 'isRequired' properties.
@override
List<APIParameter> documentOperationParameters(
APIDocumentContext context,
Operation? operation,
) {
final params = super.documentOperationParameters(context, operation)!;
params.where((p) => p.name != "scope").forEach((p) {
p.isRequired = true;
});
return params;
}
/// Generates API documentation for the responses of this controller's operations.
///
/// This method overrides the default implementation to provide custom documentation
/// for the GET and POST operations of the AuthRedirectController.
///
/// For GET requests:
/// - Documents a 200 response that serves a login form in HTML format.
///
/// For POST requests:
/// - Documents a 302 (Moved Temporarily) response for successful authorizations,
/// explaining the structure of the redirect URI for both 'code' and 'token' response types.
/// - Documents a 400 (Bad Request) response for cases where the client ID is invalid
/// and the redirect URI cannot be verified.
///
/// Parameters:
/// - context: The API documentation context.
/// - operation: The operation being documented.
///
/// Returns:
/// A Map of status codes to APIResponse objects describing the possible responses.
///
/// Throws:
/// StateError if documentation fails (i.e., for unexpected HTTP methods).
@override
Map<String, APIResponse> documentOperationResponses(
APIDocumentContext context,
Operation? operation,
) {
if (operation!.method == "GET") {
return {
"200": APIResponse.schema(
"Serves a login form.",
APISchemaObject.string(),
contentTypes: ["text/html"],
)
};
} else if (operation.method == "POST") {
return {
"${HttpStatus.movedTemporarily}": APIResponse(
"If successful, in the case of a 'response type' of 'code', the query "
"parameter of the redirect URI named 'code' contains authorization code. "
"Otherwise, the query parameter 'error' is present and contains a error string. "
"In the case of a 'response type' of 'token', the redirect URI's fragment "
"contains an access token. Otherwise, the fragment contains an error code.",
headers: {
"Location": APIHeader()
..schema = APISchemaObject.string(format: "uri")
},
),
"${HttpStatus.badRequest}": APIResponse.schema(
"If 'client_id' is invalid, the redirect URI cannot be verified and this response is sent.",
APISchemaObject.object({"error": APISchemaObject.string()}),
contentTypes: ["application/json"],
)
};
}
throw StateError("AuthRedirectController documentation failed.");
}
/// Customizes the API documentation for the operations of this controller.
///
/// This method overrides the default implementation to update the authorization URLs
/// for both the Authorization Code Flow and Implicit Flow in the API documentation.
///
/// It performs the following steps:
/// 1. Calls the superclass method to get the initial operations documentation.
/// 2. Constructs a URI from the given route.
/// 3. Sets this URI as the authorization URL for both the Authorization Code Flow
/// and the Implicit Flow in the auth server's documentation.
///
/// Parameters:
/// - context: The API documentation context.
/// - route: The route string for this controller.
/// - path: The APIPath object representing this controller's path.
///
/// Returns:
/// A Map of operation IDs to APIOperation objects describing the operations of this controller.
@override
Map<String, APIOperation> documentOperations(
APIDocumentContext context,
String route,
APIPath path,
) {
final ops = super.documentOperations(context, route, path);
final uri = Uri(path: route.substring(1));
authServer.documentedAuthorizationCodeFlow.authorizationURL = uri;
authServer.documentedImplicitFlow.authorizationURL = uri;
return ops;
}
/// Generates a redirect response for OAuth 2.0 authorization flow.
///
/// This method constructs a redirect URI based on the given parameters and the type of response
/// (code or token) requested. It handles both successful authorizations and error cases.
///
/// Parameters:
/// - [inputUri]: The base URI to redirect to. If null, falls back to the client's registered redirect URI.
/// - [clientStateOrNull]: The state parameter provided by the client for CSRF protection.
/// - [code]: The authorization code (for code flow).
/// - [token]: The access token (for token/implicit flow).
/// - [error]: Any error that occurred during the authorization process.
///
/// Returns:
/// - A [Response] object with a 302 status code and appropriate headers for redirection.
/// - If the redirect URI is invalid or cannot be constructed, returns a 400 Bad Request response.
///
/// The method constructs the redirect URI as follows:
/// - For 'code' response type: Adds code, state, and error (if any) as query parameters.
/// - For 'token' response type: Adds token details, state, and error (if any) as URI fragment.
///
/// The response includes headers to prevent caching of the redirect.
Response _redirectResponse(
String? inputUri,
String? clientStateOrNull, {
String? code,
AuthToken? token,
AuthServerException? error,
}) {
final uriString = inputUri ?? error!.client?.redirectURI;
if (uriString == null) {
return Response.badRequest(body: {"error": error!.reasonString});
}
Uri redirectURI;
try {
redirectURI = Uri.parse(uriString);
} catch (error) {
return Response.badRequest();
}
final queryParameters =
Map<String, String>.from(redirectURI.queryParameters);
String? fragment;
if (responseType == "code") {
if (code != null) {
queryParameters["code"] = code;
}
if (clientStateOrNull != null) {
queryParameters["state"] = clientStateOrNull;
}
if (error != null) {
queryParameters["error"] = error.reasonString;
}
} else if (responseType == "token") {
final params = token?.asMap() ?? {};
if (clientStateOrNull != null) {
params["state"] = clientStateOrNull;
}
if (error != null) {
params["error"] = error.reasonString;
}
fragment = params.keys
.map((key) => "$key=${Uri.encodeComponent(params[key].toString())}")
.join("&");
} else {
return _unsupportedResponseTypeResponse;
}
final responseURI = Uri(
scheme: redirectURI.scheme,
userInfo: redirectURI.userInfo,
host: redirectURI.host,
port: redirectURI.port,
path: redirectURI.path,
queryParameters: queryParameters,
fragment: fragment,
);
return Response(
HttpStatus.movedTemporarily,
{
HttpHeaders.locationHeader: responseURI.toString(),
HttpHeaders.cacheControlHeader: "no-store",
HttpHeaders.pragmaHeader: "no-cache"
},
null,
);
}
}

View file

@ -1,225 +0,0 @@
/*
* 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:convert';
/// An abstract class for parsing authorization headers.
///
/// This class defines a common interface for parsing different types of
/// authorization headers. Implementations of this class should provide
/// specific parsing logic for different authorization schemes (e.g., Bearer, Basic).
///
/// The type parameter [T] represents the return type of the [parse] method,
/// allowing for flexibility in the parsed result (e.g., String for Bearer tokens,
/// custom credential objects for other schemes).
abstract class AuthorizationParser<T> {
const AuthorizationParser();
T parse(String authorizationHeader);
}
/// Parses a Bearer token from an Authorization header.
///
/// This class extends [AuthorizationParser] and specializes in parsing Bearer tokens
/// from Authorization headers. It implements the [parse] method to extract the token
/// from a given header string.
///
/// Usage:
/// ```dart
/// final parser = AuthorizationBearerParser();
/// final token = parser.parse("Bearer myToken123");
/// print(token); // Outputs: myToken123
/// ```
///
/// If the header is invalid or missing, it throws an [AuthorizationParserException]
/// with an appropriate [AuthorizationParserExceptionReason].
class AuthorizationBearerParser extends AuthorizationParser<String?> {
const AuthorizationBearerParser();
/// Parses a Bearer token from an Authorization header.
///
/// For example, if the input to this method is "Bearer token" it would return 'token'.
///
/// If [authorizationHeader] is malformed or null, throws an [AuthorizationParserException].
@override
String? parse(String authorizationHeader) {
if (authorizationHeader.isEmpty) {
throw AuthorizationParserException(
AuthorizationParserExceptionReason.missing,
);
}
final matcher = RegExp("Bearer (.+)");
final match = matcher.firstMatch(authorizationHeader);
if (match == null) {
throw AuthorizationParserException(
AuthorizationParserExceptionReason.malformed,
);
}
return match[1];
}
}
/// A structure to hold Basic authorization credentials.
///
/// This class represents the credentials used in Basic HTTP Authentication.
/// It contains two properties: [username] and [password].
///
/// The [username] and [password] are marked as `late final`, indicating that
/// they must be initialized before use, but can only be set once.
///
/// This class is typically used in conjunction with [AuthorizationBasicParser]
/// to parse and store credentials from a Basic Authorization header.
///
/// The [toString] method is overridden to provide a string representation
/// of the credentials in the format "username:password".
///
/// Example usage:
/// ```dart
/// final credentials = AuthBasicCredentials()
/// ..username = 'john_doe'
/// ..password = 'secret123';
/// print(credentials); // Outputs: john_doe:secret123
/// ```
///
/// See [AuthorizationBasicParser] for getting instances of this type.
class AuthBasicCredentials {
/// The username of a Basic Authorization header.
late final String username;
/// The password of a Basic Authorization header.
late final String password;
@override
String toString() => "$username:$password";
}
/// Parses a Basic Authorization header.
///
/// This class extends [AuthorizationParser] and specializes in parsing Basic Authentication
/// credentials from Authorization headers. It implements the [parse] method to extract
/// the username and password from a given header string.
///
/// The parser expects the header to be in the format "Basic <base64-encoded-credentials>",
/// where the credentials are a string of "username:password" encoded in Base64.
///
/// Usage:
/// ```dart
/// final parser = AuthorizationBasicParser();
/// final credentials = parser.parse("Basic dXNlcm5hbWU6cGFzc3dvcmQ=");
/// print(credentials.username); // Outputs: username
/// print(credentials.password); // Outputs: password
/// ```
///
/// If the header is invalid, missing, or cannot be properly decoded, it throws an
/// [AuthorizationParserException] with an appropriate [AuthorizationParserExceptionReason].
class AuthorizationBasicParser
extends AuthorizationParser<AuthBasicCredentials> {
/// Creates a constant instance of [AuthorizationBasicParser].
///
/// This constructor allows for the creation of immutable instances of the parser,
/// which can be safely shared and reused across multiple parts of an application.
///
/// Example usage:
/// ```dart
/// final parser = const AuthorizationBasicParser();
/// ```
const AuthorizationBasicParser();
/// Parses a Basic Authorization header and returns [AuthBasicCredentials].
///
/// If [authorizationHeader] is malformed or null, throws an [AuthorizationParserException].
@override
AuthBasicCredentials parse(String? authorizationHeader) {
if (authorizationHeader == null) {
throw AuthorizationParserException(
AuthorizationParserExceptionReason.missing,
);
}
final matcher = RegExp("Basic (.+)");
final match = matcher.firstMatch(authorizationHeader);
if (match == null) {
throw AuthorizationParserException(
AuthorizationParserExceptionReason.malformed,
);
}
final base64String = match[1]!;
String decodedCredentials;
try {
decodedCredentials =
String.fromCharCodes(const Base64Decoder().convert(base64String));
} catch (e) {
throw AuthorizationParserException(
AuthorizationParserExceptionReason.malformed,
);
}
final splitCredentials = decodedCredentials.split(":");
if (splitCredentials.length != 2) {
throw AuthorizationParserException(
AuthorizationParserExceptionReason.malformed,
);
}
return AuthBasicCredentials()
..username = splitCredentials.first
..password = splitCredentials.last;
}
}
/// Enumerates the possible reasons for authorization parsing failures.
///
/// This enum is used in conjunction with [AuthorizationParserException] to
/// provide more specific information about why the parsing of an authorization
/// header failed.
///
/// The enum contains two values:
/// - [missing]: Indicates that the required authorization header was not present.
/// - [malformed]: Indicates that the authorization header was present but its
/// format was incorrect or could not be properly parsed.
///
/// This enum is typically used by [AuthorizationBearerParser] and
/// [AuthorizationBasicParser] to specify the nature of parsing failures.
enum AuthorizationParserExceptionReason { missing, malformed }
/// An exception class for errors encountered during authorization parsing.
///
/// This exception is thrown when there's an issue parsing an authorization header.
/// It contains a [reason] field of type [AuthorizationParserExceptionReason]
/// which provides more specific information about why the parsing failed.
///
/// The [reason] can be either [AuthorizationParserExceptionReason.missing]
/// (indicating the absence of a required authorization header) or
/// [AuthorizationParserExceptionReason.malformed] (indicating an incorrectly
/// formatted authorization header).
///
/// This exception is typically thrown by implementations of [AuthorizationParser],
/// such as [AuthorizationBearerParser] and [AuthorizationBasicParser].
///
/// Example usage:
/// ```dart
/// try {
/// parser.parse(header);
/// } catch (e) {
/// if (e is AuthorizationParserException) {
/// if (e.reason == AuthorizationParserExceptionReason.missing) {
/// print('Authorization header is missing');
/// } else if (e.reason == AuthorizationParserExceptionReason.malformed) {
/// print('Authorization header is malformed');
/// }
/// }
/// }
/// ```
class AuthorizationParserException implements Exception {
AuthorizationParserException(this.reason);
AuthorizationParserExceptionReason reason;
}

File diff suppressed because it is too large Load diff

View file

@ -1,374 +0,0 @@
/*
* 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 'dart:io';
import 'package:protevus_openapi/documentable.dart';
import 'package:protevus_auth/auth.dart';
import 'package:protevus_http/http.dart';
import 'package:protevus_openapi/v3.dart';
/// A [Controller] that validates the Authorization header of a request.
///
/// This class, Authorizer, is responsible for authenticating and authorizing incoming HTTP requests.
/// It validates the Authorization header, processes it according to the specified parser (e.g., Bearer or Basic),
/// and then uses the provided validator to check the credentials.
///
/// For each request, this controller parses the authorization header, validates it with an [AuthValidator] and then create an [Authorization] object
/// if successful. The [Request] keeps a reference to this [Authorization] and is then sent to the next controller in the channel.
///
/// If either parsing or validation fails, a 401 Unauthorized response is sent and the [Request] is removed from the channel.
///
/// Parsing occurs according to [parser]. The resulting value (e.g., username and password) is sent to [validator].
/// [validator] verifies this value (e.g., lookup a user in the database and verify their password matches).
///
/// Usage:
///
/// router
/// .route("/protected-route")
/// .link(() =>new Authorizer.bearer(authServer))
/// .link(() => new ProtectedResourceController());
class Authorizer extends Controller {
/// Creates an instance of [Authorizer].
///
/// This constructor allows for creating an [Authorizer] with custom configurations.
///
/// By default, this instance will parse bearer tokens from the authorization header, e.g.:
///
/// Authorization: Bearer ap9ijlarlkz8jIOa9laweo
///
/// If [scopes] is provided, the authorization granted must have access to *all* scopes according to [validator].
Authorizer(
this.validator, {
this.parser = const AuthorizationBearerParser(),
List<String>? scopes,
}) : scopes = scopes?.map((s) => AuthScope(s)).toList();
/// Creates an instance of [Authorizer] with Basic Authentication parsing.
///
/// This constructor initializes an [Authorizer] that uses Basic Authentication.
/// It sets up the [Authorizer] to parse the Authorization header of incoming requests
/// using the [AuthorizationBasicParser].
///
/// The Authorization header for Basic Authentication should be in the format:
///
/// Authorization: Basic base64(username:password)
Authorizer.basic(AuthValidator? validator)
: this(validator, parser: const AuthorizationBasicParser());
/// Creates an instance of [Authorizer] with Bearer token parsing.
///
/// This constructor initializes an [Authorizer] that uses Bearer token authentication.
/// It sets up the [Authorizer] to parse the Authorization header of incoming requests
/// using the [AuthorizationBearerParser].
///
/// Authorization: Bearer ap9ijlarlkz8jIOa9laweo
///
/// If [scopes] is provided, the bearer token must have access to *all* scopes according to [validator].
Authorizer.bearer(AuthValidator? validator, {List<String>? scopes})
: this(
validator,
parser: const AuthorizationBearerParser(),
scopes: scopes,
);
/// The validating authorization object.
///
/// This property holds an instance of [AuthValidator] responsible for validating
/// the credentials parsed from the Authorization header. It processes these
/// credentials and produces an [Authorization] object that represents the
/// authorization level of the provided credentials.
///
/// The validator can also reject a request if the credentials are invalid or
/// insufficient. This property is typically set to an instance of [AuthServer].
///
/// The validator is crucial for determining whether a request should be allowed
/// to proceed based on the provided authorization information.
final AuthValidator? validator;
/// The list of required scopes for authorization.
///
/// If [validator] grants scope-limited authorizations (e.g., OAuth2 bearer tokens), the authorization
/// provided by the request's header must have access to all [scopes] in order to move on to the next controller.
///
/// This property is set with a list of scope strings in a constructor. Each scope string is parsed into
/// an [AuthScope] and added to this list.
final List<AuthScope>? scopes;
/// Parses the Authorization header of incoming requests.
///
/// The parser determines how to interpret the data in the Authorization header. Concrete subclasses
/// are [AuthorizationBasicParser] and [AuthorizationBearerParser].
///
/// Once parsed, the parsed value is validated by [validator].
final AuthorizationParser parser;
/// Handles the incoming request by validating its authorization.
///
/// This method performs the following steps:
/// 1. Extracts the Authorization header from the request.
/// 2. If the header is missing, returns an unauthorized response.
/// 3. Attempts to parse the authorization data using the configured parser.
/// 4. Validates the parsed data using the configured validator.
/// 5. If validation succeeds, adds the authorization to the request and proceeds.
/// 6. If validation fails due to insufficient scope, returns a forbidden response.
/// 7. For other validation failures, returns an unauthorized response.
/// 8. Handles parsing exceptions by returning appropriate error responses.
///
/// @param request The incoming HTTP request to be authorized.
/// @return A [Future] that resolves to either the authorized [Request] or an error [Response].
@override
FutureOr<RequestOrResponse> handle(Request request) async {
final authData = request.raw.headers.value(HttpHeaders.authorizationHeader);
if (authData == null) {
return Response.unauthorized();
}
try {
final value = parser.parse(authData);
request.authorization =
await validator!.validate(parser, value, requiredScope: scopes);
if (request.authorization == null) {
return Response.unauthorized();
}
_addScopeRequirementModifier(request);
} on AuthorizationParserException catch (e) {
return _responseFromParseException(e);
} on AuthServerException catch (e) {
if (e.reason == AuthRequestError.invalidScope) {
return Response.forbidden(
body: {
"error": "insufficient_scope",
"scope": scopes!.map((s) => s.toString()).join(" ")
},
);
}
return Response.unauthorized();
}
return request;
}
/// Generates an appropriate HTTP response based on the type of AuthorizationParserException.
///
/// This method takes an [AuthorizationParserException] as input and returns
/// a [Response] object based on the exception's reason:
///
/// - For [AuthorizationParserExceptionReason.malformed], it returns a 400 Bad Request
/// response with a body indicating an invalid authorization header.
/// - For [AuthorizationParserExceptionReason.missing], it returns a 401 Unauthorized
/// response.
/// - For any other reason, it returns a 500 Server Error response.
///
/// @param e The AuthorizationParserException that occurred during parsing.
/// @return A Response object appropriate to the exception reason.
Response _responseFromParseException(AuthorizationParserException e) {
switch (e.reason) {
case AuthorizationParserExceptionReason.malformed:
return Response.badRequest(
body: {"error": "invalid_authorization_header"},
);
case AuthorizationParserExceptionReason.missing:
return Response.unauthorized();
default:
return Response.serverError();
}
}
/// Adds a response modifier to the request to handle scope requirements.
///
/// This method is called after successful authorization and adds a response
/// modifier to the request. The modifier's purpose is to enhance 403 (Forbidden)
/// responses that are due to insufficient scope.
///
/// If this [Authorizer] has required scopes and the response is a 403 with a body
/// containing a "scope" key, this modifier will add any of this [Authorizer]'s
/// required scopes that aren't already present in the response body's scope list.
///
/// This ensures that if a downstream controller returns a 403 due to insufficient
/// scope, the response includes all the scopes required by both this [Authorizer]
/// and the downstream controller.
///
/// @param request The [Request] object to which the modifier will be added.
void _addScopeRequirementModifier(Request request) {
// If a controller returns a 403 because of invalid scope,
// this Authorizer adds its required scope as well.
if (scopes != null) {
request.addResponseModifier((resp) {
if (resp.statusCode == 403 && resp.body is Map) {
final body = resp.body as Map<String, dynamic>;
if (body.containsKey("scope")) {
final declaredScopes = (body["scope"] as String).split(" ");
final scopesToAdd = scopes!
.map((s) => s.toString())
.where((s) => !declaredScopes.contains(s));
body["scope"] =
[scopesToAdd, declaredScopes].expand((i) => i).join(" ");
}
}
});
}
}
/// Documents the components for the API documentation.
///
/// This method is responsible for registering custom API responses that are specific
/// to authorization-related errors. It adds three responses to the API documentation:
///
/// 1. "InsufficientScope": Used when the provided credentials or bearer token have
/// insufficient permissions to access a route.
///
/// 2. "InsufficientAccess": Used when the provided credentials or bearer token are
/// not authorized for a specific request.
///
/// 3. "MalformedAuthorizationHeader": Used when the provided Authorization header
/// is malformed.
///
/// Each response is registered with a description and a schema defining the
/// structure of the JSON response body.
///
/// @param context The APIDocumentContext used to register the responses.
@override
void documentComponents(APIDocumentContext context) {
/// Calls the superclass's documentComponents method.
///
/// This method invokes the documentComponents method of the superclass,
/// ensuring that any component documentation defined in the parent class
/// is properly registered in the API documentation context.
///
/// @param context The APIDocumentContext used for registering API components.
super.documentComponents(context);
/// Registers an "InsufficientScope" response in the API documentation.
///
/// This response is used when the provided credentials or bearer token
/// have insufficient permissions to access a specific route. It includes
/// details about the error and the required scope for the operation.
///
/// The response has the following structure:
/// - A description explaining the insufficient scope error.
/// - Content of type "application/json" with a schema containing:
/// - An "error" field of type string.
/// - A "scope" field of type string, describing the required scope.
///
/// This response can be referenced in API operations to standardize
/// the documentation of insufficient scope errors.
context.responses.register(
"InsufficientScope",
APIResponse(
"The provided credentials or bearer token have insufficient permission to access this route.",
content: {
"application/json": APIMediaType(
schema: APISchemaObject.object({
"error": APISchemaObject.string(),
"scope": APISchemaObject.string()
..description = "The required scope for this operation."
}),
)
},
),
);
/// Registers an "InsufficientAccess" response in the API documentation.
///
/// This response is used when the provided credentials or bearer token
/// are not authorized for a specific request. It includes details about
/// the error in a JSON format.
///
/// The response has the following structure:
/// - A description explaining the insufficient access error.
/// - Content of type "application/json" with a schema containing:
/// - An "error" field of type string.
///
/// This response can be referenced in API operations to standardize
/// the documentation of insufficient access errors.
context.responses.register(
"InsufficientAccess",
APIResponse(
"The provided credentials or bearer token are not authorized for this request.",
content: {
"application/json": APIMediaType(
schema: APISchemaObject.object(
{"error": APISchemaObject.string()},
),
)
},
),
);
/// Registers a "MalformedAuthorizationHeader" response in the API documentation.
///
/// This response is used when the provided Authorization header is malformed.
/// It includes details about the error in a JSON format.
///
/// The response has the following structure:
/// - A description explaining the malformed authorization header error.
/// - Content of type "application/json" with a schema containing:
/// - An "error" field of type string.
///
/// This response can be referenced in API operations to standardize
/// the documentation of malformed authorization header errors.
context.responses.register(
"MalformedAuthorizationHeader",
APIResponse(
"The provided Authorization header was malformed.",
content: {
"application/json": APIMediaType(
schema: APISchemaObject.object(
{"error": APISchemaObject.string()},
),
)
},
),
);
}
/// Documents the operations for the API documentation.
///
/// This method is responsible for adding security-related responses and requirements
/// to each operation in the API documentation. It performs the following tasks:
///
/// 1. Calls the superclass's documentOperations method to get the base operations.
/// 2. For each operation:
/// - Adds a 400 response for malformed authorization headers.
/// - Adds a 401 response for insufficient access.
/// - Adds a 403 response for insufficient scope.
/// - Retrieves security requirements from the validator.
/// - Adds these security requirements to the operation.
///
/// @param context The APIDocumentContext used for documenting the API.
/// @param route The route string for which operations are being documented.
/// @param path The APIPath object representing the path of the operations.
/// @return A map of operation names to APIOperation objects with added security documentation.
@override
Map<String, APIOperation> documentOperations(
APIDocumentContext context,
String route,
APIPath path,
) {
final operations = super.documentOperations(context, route, path);
operations.forEach((_, op) {
op.addResponse(400, context.responses["MalformedAuthorizationHeader"]);
op.addResponse(401, context.responses["InsufficientAccess"]);
op.addResponse(403, context.responses["InsufficientScope"]);
final requirements = validator!
.documentRequirementsForAuthorizer(context, this, scopes: scopes);
for (final req in requirements) {
op.addSecurityRequirement(req);
}
});
return operations;
}
}

View file

@ -1,242 +0,0 @@
/*
* 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_auth/auth.dart';
/// An exception class for handling authentication server errors.
///
/// This class implements the [Exception] interface and is used to represent
/// various errors that can occur during the authentication process.
///
/// The [AuthServerException] contains:
/// - [reason]: An [AuthRequestError] enum value representing the specific error.
/// - [client]: An optional [AuthClient] associated with the error.
///
/// It also provides utility methods:
/// - [errorString]: A static method that converts [AuthRequestError] enum values to standardized error strings.
/// - [reasonString]: A getter that returns the error string for the current [reason].
///
/// The [toString] method is overridden to provide a custom string representation of the exception.
class AuthServerException implements Exception {
/// Creates an [AuthServerException] with the specified [reason] and optional [client].
///
/// The [reason] parameter is an [AuthRequestError] enum value representing the specific error.
/// The [client] parameter is an optional [AuthClient] associated with the error.
///
/// Example:
/// ```dart
/// var exception = AuthServerException(AuthRequestError.invalidRequest, null);
/// ```
AuthServerException(this.reason, this.client);
/// Converts an [AuthRequestError] enum value to its corresponding string representation.
///
/// This static method takes an [AuthRequestError] as input and returns a standardized
/// string that represents the error. These strings are suitable for inclusion in
/// query strings or JSON response bodies when indicating errors during the processing
/// of OAuth 2.0 requests.
///
/// The returned strings conform to the error codes defined in the OAuth 2.0 specification,
/// with the exception of 'invalid_token', which is a custom addition.
///
/// Example:
/// ```dart
/// var errorString = AuthServerException.errorString(AuthRequestError.invalidRequest);
/// print(errorString); // Outputs: "invalid_request"
/// ```
///
/// @param error The [AuthRequestError] enum value to convert.
/// @return A string representation of the error.
static String errorString(AuthRequestError error) {
switch (error) {
case AuthRequestError.invalidRequest:
return "invalid_request";
case AuthRequestError.invalidClient:
return "invalid_client";
case AuthRequestError.invalidGrant:
return "invalid_grant";
case AuthRequestError.invalidScope:
return "invalid_scope";
case AuthRequestError.invalidToken:
return "invalid_token";
case AuthRequestError.unsupportedGrantType:
return "unsupported_grant_type";
case AuthRequestError.unsupportedResponseType:
return "unsupported_response_type";
case AuthRequestError.unauthorizedClient:
return "unauthorized_client";
case AuthRequestError.accessDenied:
return "access_denied";
case AuthRequestError.serverError:
return "server_error";
case AuthRequestError.temporarilyUnavailable:
return "temporarily_unavailable";
}
}
/// The specific reason for the authentication error.
///
/// This property holds an [AuthRequestError] enum value that represents
/// the specific error that occurred during the authentication process.
/// It provides detailed information about why the authentication request failed.
AuthRequestError reason;
/// The optional [AuthClient] associated with this exception.
///
/// This property may contain an [AuthClient] instance that is related to the
/// authentication error. It can be null if no specific client is associated
/// with the error or if the error occurred before client authentication.
///
/// This information can be useful for debugging or logging purposes, providing
/// context about which client encountered the authentication error.
AuthClient? client;
/// Returns a string representation of the [reason] for this exception.
///
/// This getter utilizes the static [errorString] method to convert the
/// [AuthRequestError] enum value stored in [reason] to its corresponding
/// string representation.
///
/// @return A standardized string representation of the error reason.
String get reasonString {
return errorString(reason);
}
/// Returns a string representation of the [AuthServerException].
///
/// This method overrides the default [Object.toString] method to provide
/// a custom string representation of the exception. The returned string
/// includes the exception class name, the [reason] for the exception,
/// and the associated [client] (if any).
///
/// @return A string in the format "AuthServerException: [reason] [client]".
@override
String toString() {
return "AuthServerException: $reason $client";
}
}
/// Enum representing possible errors as defined by the OAuth 2.0 specification.
///
/// Auth endpoints will use this list of values to determine the response sent back
/// to a client upon a failed request.
enum AuthRequestError {
/// Represents an invalid request error.
///
/// The request is missing a required parameter, includes an
/// unsupported parameter value (other than grant type),
/// repeats a parameter, includes multiple credentials,
/// utilizes more than one mechanism for authenticating the
/// client, or is otherwise malformed.
invalidRequest,
/// Represents an invalid client error.
///
/// Client authentication failed (e.g., unknown client, no
/// client authentication included, or unsupported
/// authentication method). The authorization server MAY
/// return an HTTP 401 (Unauthorized) status code to indicate
/// which HTTP authentication schemes are supported. If the
/// client attempted to authenticate via the "Authorization"
/// request header field, the authorization server MUST
/// respond with an HTTP 401 (Unauthorized) status code and
/// include the "WWW-Authenticate" response header field
/// matching the authentication scheme used by the client.
invalidClient,
/// Represents an invalid grant error.
///
/// The provided authorization grant (e.g., authorization
/// code, resource owner credentials) or refresh token is
/// invalid, expired, revoked, does not match the redirection
/// URI used in the authorization request, or was issued to
/// another client.
invalidGrant,
/// Represents an invalid scope error.
///
/// This error occurs when the requested scope is invalid, unknown, malformed,
/// or exceeds the scope granted by the resource owner. It typically indicates
/// that the client has requested access to resources or permissions that are
/// either not recognized by the authorization server or not authorized for
/// the particular client or user.
///
/// In the OAuth 2.0 flow, this error might be returned if a client requests
/// access to a scope that doesn't exist or that the user hasn't granted
/// permission for.
invalidScope,
/// Represents an unsupported grant type error.
///
/// This error occurs when the authorization server does not support the
/// grant type requested by the client. It typically indicates that the
/// client has specified a grant type that is either not recognized or
/// not implemented by the authorization server.
///
/// In the OAuth 2.0 flow, this error might be returned if, for example,
/// a client requests a grant type like "password" when the server only
/// supports "authorization_code" and "refresh_token" grant types.
unsupportedGrantType,
/// Represents an unsupported response type error.
///
/// This error occurs when the authorization server does not support obtaining
/// an authorization code using the specified response type. It typically
/// indicates that the client has requested a response type that is not
/// recognized or not implemented by the authorization server.
unsupportedResponseType,
/// Represents an unauthorized client error.
///
/// This error occurs when the client is not authorized to request an
/// authorization code using this method. It typically indicates that
/// the client does not have the necessary permissions or credentials
/// to perform the requested action, even though it may be properly
/// authenticated.
unauthorizedClient,
/// Represents an access denied error.
///
/// This error occurs when the resource owner or authorization server denies the request.
/// It is typically used when the authenticated user does not have sufficient permissions
/// to perform the requested action, or when the user explicitly denies authorization
/// during the OAuth flow.
accessDenied,
/// Represents a server error.
///
/// This error occurs when the authorization server encounters an unexpected
/// condition that prevented it from fulfilling the request. This is typically
/// used for internal server errors or other unexpected issues that prevent
/// the server from properly processing the authentication request.
serverError,
/// Represents a temporarily unavailable error.
///
/// This error occurs when the authorization server is temporarily unable to handle
/// the request due to a temporary overloading or maintenance of the server.
/// The client may repeat the request at a later time. The server SHOULD include
/// a Retry-After HTTP header field in the response indicating how long the client
/// should wait before retrying the request.
temporarilyUnavailable,
/// Represents an invalid token error.
///
/// This error occurs when the provided token is invalid, expired, or otherwise
/// not acceptable for the requested operation. It is typically used when a client
/// presents an access token that cannot be validated or is no longer valid.
///
/// Note: This particular error reason is not part of the standard OAuth 2.0
/// specification. It is a custom addition to handle scenarios specific to
/// token validation that are not covered by other standard error types.
invalidToken
}

File diff suppressed because it is too large Load diff

View file

@ -1,217 +0,0 @@
/*
* 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_auth/auth.dart';
/// Defines the interface for a Resource Owner in OAuth 2.0 authentication.
///
/// Your application's 'user' type must implement the methods declared in this interface. [AuthServer] can
/// validate the credentials of a [ResourceOwner] to grant authorization codes and access tokens on behalf of that
/// owner.
abstract class ResourceOwner {
/// The username of the resource owner.
///
/// This property represents the unique identifier for a resource owner, typically used for authentication purposes.
/// It must be unique among all resource owners in the system. Often, this value is an email address.
///
/// The username is used by authenticating users to identify their account when logging in or performing
/// other authentication-related actions.
///
/// This property is nullable, which means it can be null in some cases, such as when creating a new
/// resource owner instance before setting the username.
String? username;
/// The hashed password of this instance.
///
/// This property stores the password of the resource owner in a hashed format.
/// Hashing is a one-way process that converts the plain text password into a
/// fixed-length string of characters, which is more secure to store than the
/// original password.
///
/// The hashed password is used for password verification during authentication
/// without storing the actual password. This enhances security by ensuring
/// that even if the database is compromised, the original passwords remain
/// unknown.
///
/// This property is nullable, allowing for cases where a password might not
/// be set or required for certain types of resource owners.
String? hashedPassword;
/// The salt used in the hashing process for [hashedPassword].
///
/// A salt is a random string that is added to the password before hashing,
/// which adds an extra layer of security to the hashed password. It helps
/// protect against rainbow table attacks and ensures that even if two users
/// have the same password, their hashed passwords will be different.
///
/// This property is nullable to accommodate cases where a salt might not be
/// used or stored separately from the hashed password.
String? salt;
/// A unique identifier of this resource owner.
///
/// This property represents a unique identifier for the resource owner, typically
/// used in authentication and authorization processes. The [AuthServer] uses this
/// identifier to associate authorization codes and access tokens with the specific
/// resource owner.
///
/// The identifier is of type [int] and is nullable, allowing for cases where an ID
/// might not be assigned yet (e.g., when creating a new resource owner instance).
///
/// It's crucial to ensure that this ID remains unique across all resource owners
/// in the system to maintain the integrity of the authentication and authorization
/// processes.
///
/// This getter method should be implemented to return the unique identifier of
/// the resource owner.
int? get id;
}
/// The methods used by an [AuthServer] to store information and customize behavior related to authorization.
///
/// An [AuthServer] requires an instance of this type to manage storage of [ResourceOwner]s, [AuthToken], [AuthCode],
/// and [AuthClient]s. You may also customize the token format or add more granular authorization scope rules.
///
/// Prefer to use `ManagedAuthDelegate` from 'package:conduit_core/managed_auth.dart' instead of implementing this interface;
/// there are important details to consider and test when implementing this interface.
///
/// This abstract class defines the contract for implementing an authentication and authorization system.
/// It provides methods for managing resource owners, clients, tokens, and authorization codes.
/// Implementations of this class are responsible for handling the storage, retrieval, and management
/// of these entities, as well as customizing certain behaviors of the authentication process.
///
/// Key responsibilities include:
/// - Managing resource owners (users)
/// - Handling client applications
/// - Storing and retrieving access and refresh tokens
/// - Managing authorization codes
/// - Customizing token formats and allowed scopes
///
/// Each method in this class corresponds to a specific operation in the OAuth 2.0 flow,
/// allowing for a flexible and extensible authentication system.
abstract class AuthServerDelegate {
/// Retrieves a [ResourceOwner] based on the provided [username].
///
/// This method must return an instance of [ResourceOwner] if one exists for [username]. Otherwise, it must return null.
///
/// Every property declared by [ResourceOwner] must be non-null in the return value.
///
/// [server] is the [AuthServer] invoking this method.
FutureOr<ResourceOwner?> getResourceOwner(AuthServer server, String username);
/// Stores a new [AuthClient] in the system.
///
/// [client] must be returned by [getClient] after this method has been invoked, and until (if ever)
/// [removeClient] is invoked.
FutureOr addClient(AuthServer server, AuthClient client);
/// Retrieves an [AuthClient] based on the provided client ID.
///
/// This method must return an instance of [AuthClient] if one exists for [clientID]. Otherwise, it must return null.
/// [server] is the [AuthServer] requesting the [AuthClient].
FutureOr<AuthClient?> getClient(AuthServer server, String clientID);
/// Removes an [AuthClient] for a given client ID.
///
/// This method must delete the [AuthClient] for [clientID]. Subsequent requests to this
/// instance for [getClient] must return null after this method completes. If there is no
/// matching [clientID], this method may choose whether to throw an exception or fail silently.
///
/// [server] is the [AuthServer] requesting the [AuthClient].
FutureOr removeClient(AuthServer server, String clientID);
/// Retrieves an [AuthToken] based on either its access token or refresh token.
///
/// Exactly one of [byAccessToken] and [byRefreshToken] may be non-null, if not, this method must throw an error.
///
/// If [byAccessToken] is not-null and there exists a matching [AuthToken.accessToken], return that token.
/// If [byRefreshToken] is not-null and there exists a matching [AuthToken.refreshToken], return that token.
///
/// If no match is found, return null.
///
/// [server] is the [AuthServer] requesting the [AuthToken].
FutureOr<AuthToken?> getToken(
AuthServer server, {
String? byAccessToken,
String? byRefreshToken,
});
/// Deletes all [AuthToken]s and [AuthCode]s associated with a specific [ResourceOwner].
///
/// [server] is the requesting [AuthServer]. [resourceOwnerID] is the [ResourceOwner.id].
FutureOr removeTokens(AuthServer server, int resourceOwnerID);
/// Deletes an [AuthToken] that was granted by a specific [AuthCode].
///
/// If an [AuthToken] has been granted by exchanging [AuthCode], that token must be revoked
/// and can no longer be used to authorize access to a resource. [grantedByCode] should
/// also be removed.
///
/// This method is invoked when attempting to exchange an authorization code that has already granted a token.
FutureOr removeToken(AuthServer server, AuthCode grantedByCode);
/// Stores an [AuthToken] in the system.
///
/// [token] must be stored such that it is accessible from [getToken], and until it is either
/// revoked via [removeToken] or [removeTokens], or until it has expired and can reasonably
/// be believed to no longer be in use.
///
/// You may alter [token] prior to storing it. This may include replacing [AuthToken.accessToken] with another token
/// format. The default token format will be a random 32 character string.
///
/// If this token was granted through an authorization code, [issuedFrom] is that code. Otherwise, [issuedFrom]
/// is null.
FutureOr addToken(AuthServer server, AuthToken token, {AuthCode? issuedFrom});
/// Updates an existing [AuthToken] with new values.
///
/// This method must must update an existing [AuthToken], found by [oldAccessToken],
/// with the values [newAccessToken], [newIssueDate] and [newExpirationDate].
///
/// You may alter the token in addition to the provided values, and you may override the provided values.
/// [newAccessToken] defaults to a random 32 character string.
FutureOr updateToken(
AuthServer server,
String? oldAccessToken,
String? newAccessToken,
DateTime? newIssueDate,
DateTime? newExpirationDate,
);
/// Stores an [AuthCode] in the system.
///
/// [code] must be accessible until its expiration date.
FutureOr addCode(AuthServer server, AuthCode code);
/// Retrieves an [AuthCode] based on its identifying code.
///
/// This must return an instance of [AuthCode] where [AuthCode.code] matches [code].
/// Return null if no matching code.
FutureOr<AuthCode?> getCode(AuthServer server, String code);
/// Removes an [AuthCode] from the system based on its identifying code.
///
/// The [AuthCode.code] matching [code] must be deleted and no longer accessible.
FutureOr removeCode(AuthServer server, String? code);
/// Returns a list of allowed scopes for a given [ResourceOwner].
///
/// Subclasses override this method to return a list of [AuthScope]s based on some attribute(s) of an [ResourceOwner].
/// That [ResourceOwner] is then restricted to only those scopes, even if the authenticating client would allow other scopes
/// or scopes with higher privileges.
///
/// By default, this method returns [AuthScope.any] - any [ResourceOwner] being authenticated has full access to the scopes
/// available to the authenticating client.
///
/// When overriding this method, it is important to note that (by default) only the properties declared by [ResourceOwner]
/// will be valid for [owner]. If [owner] has properties that are application-specific (like a `role`),
/// [getResourceOwner] must also be overridden to ensure those values are fetched.
List<AuthScope>? getAllowedScopes(ResourceOwner owner) => AuthScope.any;
}

View file

@ -1,50 +0,0 @@
/*
* 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_auth/auth.dart';
import 'package:protevus_http/http.dart';
import 'package:protevus_openapi/v3.dart';
/// A mixin that defines the interface for validating authorization data.
///
/// When an [Authorizer] processes a [Request], it invokes [validate], passing in the parsed Authorization
/// header of the [Request].
///
/// [AuthServer] implements this interface.
mixin AuthValidator {
/// Validates authorization data and returns an [Authorization] if valid.
///
/// This method is invoked by [Authorizer] to validate the Authorization header of a request. [authorizationData]
/// is the parsed contents of the Authorization header, while [parser] is the object that parsed the header.
///
/// If this method returns null, an [Authorizer] will send a 401 Unauthorized response.
/// If this method throws an [AuthorizationParserException], a 400 Bad Request response is sent.
/// If this method throws an [AuthServerException], an appropriate status code is sent for the details of the exception.
///
/// If [requiredScope] is provided, a request's authorization must have at least that much scope to pass the [Authorizer].
FutureOr<Authorization?> validate<T>(
AuthorizationParser<T> parser,
T authorizationData, {
List<AuthScope>? requiredScope,
});
/// Provides [APISecurityRequirement]s for the given [authorizer].
///
/// An [Authorizer] that adds security requirements to operations will invoke this method to allow this validator to define those requirements.
/// The [Authorizer] must provide the [context] it was given to document the operations, itself and optionally a list of [scopes] required to pass it.
List<APISecurityRequirement> documentRequirementsForAuthorizer(
APIDocumentContext context,
Authorizer authorizer, {
List<AuthScope>? scopes,
}) =>
[];
}

View file

@ -1,21 +0,0 @@
name: protevus_auth
description: The Authentication Package for the Protevus Platform
version: 0.0.1
homepage: https://protevus.com
documentation: https://docs.protevus.com
repository: https://git.protevus.com/protevus/platform
environment:
sdk: ^3.4.2
# Add regular dependencies here.
dependencies:
protevus_http: ^0.0.1
protevus_openapi: ^0.0.1
protevus_hashing: ^0.0.1
crypto: ^3.0.3
# path: ^1.8.0
dev_dependencies:
lints: ^3.0.0
test: ^1.24.0

View file

@ -1,7 +0,0 @@
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/
# Avoid committing pubspec.lock for library packages; see
# https://dart.dev/guides/libraries/private-files#pubspeclock.
pubspec.lock

View file

@ -1,3 +0,0 @@
## 1.0.0
- Initial version.

View file

@ -1,10 +0,0 @@
The MIT License (MIT)
The Laravel Framework is Copyright (c) Taylor Otwell
The Fabric Framework is Copyright (c) Vieo, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1 +0,0 @@
<p align="center"><a href="https://protevus.com" target="_blank"><img src="https://git.protevus.com/protevus/branding/raw/branch/main/protevus-logo-bg.png"></a></p>

View file

@ -1,30 +0,0 @@
# This file configures the static analysis results for your project (errors,
# warnings, and lints).
#
# This enables the 'recommended' set of lints from `package:lints`.
# This set helps identify many issues that may lead to problems when running
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
# style and format.
#
# If you want a smaller set of lints you can change this to specify
# 'package:lints/core.yaml'. These are just the most critical lints
# (the recommended set includes the core lints).
# The core lints are also what is used by pub.dev for scoring packages.
include: package:lints/recommended.yaml
# Uncomment the following section to specify additional rules.
# linter:
# rules:
# - camel_case_types
# analyzer:
# exclude:
# - path/to/excluded/files/**
# For more information about the core and recommended set of lints, see
# https://dart.dev/go/core-lints
# For additional information about configuring this file, see
# https://dart.dev/guides/language/analysis-options

View file

@ -1,30 +0,0 @@
/*
* 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.
*/
/// Configuration library for the Protevus Platform.
///
/// This library exports various components related to configuration management,
/// including compiler, runtime, and default configurations. It also includes
/// utilities for handling intermediate exceptions and mirror properties.
///
/// The exported modules are:
/// - compiler: Handles compilation of configuration files.
/// - configuration: Defines the core configuration structure.
/// - default_configurations: Provides pre-defined default configurations.
/// - intermediate_exception: Manages exceptions during configuration processing.
/// - mirror_property: Utilities for reflection-based property handling.
/// - runtime: Manages runtime configuration aspects.
library config;
export 'package:protevus_config/src/compiler.dart';
export 'package:protevus_config/src/configuration.dart';
export 'package:protevus_config/src/default_configurations.dart';
export 'package:protevus_config/src/intermediate_exception.dart';
export 'package:protevus_config/src/mirror_property.dart';
export 'package:protevus_config/src/runtime.dart';

View file

@ -1,72 +0,0 @@
/*
* 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:io';
import 'dart:mirrors';
import 'package:protevus_config/config.dart';
import 'package:protevus_runtime/runtime.dart';
/// A compiler class for configurations in the Protevus Platform.
///
/// This class extends the [Compiler] class and provides functionality to
/// compile configuration-related data and modify package files.
///
/// The [compile] method creates a map of configuration names to their
/// corresponding [ConfigurationRuntimeImpl] instances by scanning for
/// subclasses of [Configuration] in the given [MirrorContext].
///
/// The [deflectPackage] method modifies the "conduit_config.dart" file in the
/// destination directory by removing a specific export statement.
class ConfigurationCompiler extends Compiler {
/// Compiles configuration data from the given [MirrorContext].
///
/// This method scans the [context] for all subclasses of [Configuration]
/// and creates a map where:
/// - The keys are the names of these subclasses (as strings)
/// - The values are instances of [ConfigurationRuntimeImpl] created from
/// the corresponding subclass
///
/// Returns a [Map<String, Object>] where each entry represents a
/// configuration class and its runtime implementation.
@override
Map<String, Object> compile(MirrorContext context) {
return Map.fromEntries(
context.getSubclassesOf(Configuration).map((c) {
return MapEntry(
MirrorSystem.getName(c.simpleName),
ConfigurationRuntimeImpl(c),
);
}),
);
}
/// Modifies the package file by removing a specific export statement.
///
/// This method performs the following steps:
/// 1. Locates the "config.dart" file in the "lib/" directory of the [destinationDirectory].
/// 2. Reads the contents of the file.
/// 3. Removes the line "export 'package:protevus_config/src/compiler.dart';" from the file contents.
/// 4. Writes the modified contents back to the file.
///
/// This operation is typically used to adjust the exported modules in the compiled package.
///
/// [destinationDirectory] is the directory where the package files are located.
@override
void deflectPackage(Directory destinationDirectory) {
final libFile = File.fromUri(
destinationDirectory.uri.resolve("lib/").resolve("config.dart"),
);
final contents = libFile.readAsStringSync();
libFile.writeAsStringSync(
contents.replaceFirst(
"export 'package:protevus_config/src/compiler.dart';", ""),
);
}
}

View file

@ -1,627 +0,0 @@
/*
* 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:io';
import 'package:protevus_config/config.dart';
import 'package:protevus_runtime/runtime.dart';
import 'package:meta/meta.dart';
import 'package:yaml/yaml.dart';
/// A base class for configuration management in Dart applications.
///
/// [Configuration] provides a framework for reading and parsing YAML-based
/// configuration files or strings. It offers various constructors to create
/// configuration objects from different sources (maps, strings, or files),
/// and includes methods for decoding and validating configuration data.
///
/// Key features:
/// - Supports creating configurations from YAML strings, files, or maps
/// - Provides a runtime context for configuration-specific operations
/// - Includes a default decoding mechanism that can be overridden
/// - Offers a validation method to ensure all required fields are present
/// - Allows for environment variable substitution in configuration values
///
/// Subclasses of [Configuration] should implement specific configuration
/// structures by defining properties that correspond to expected YAML keys.
/// The [decode] and [validate] methods can be overridden to provide custom
/// behavior for complex configuration scenarios.
///
/// Example usage:
/// ```dart
/// class MyConfig extends Configuration {
/// late String apiKey;
/// int port = 8080;
///
/// @override
/// void validate() {
/// super.validate();
/// if (port < 1000 || port > 65535) {
/// throw ConfigurationException(this, "Invalid port number");
/// }
/// }
/// }
///
/// final config = MyConfig.fromFile(File('config.yaml'));
/// ```
abstract class Configuration {
/// Default constructor for the Configuration class.
///
/// This constructor creates a new instance of the Configuration class
/// without any initial configuration data. It can be used as a starting
/// point for creating custom configurations, which can then be populated
/// using other methods or by setting properties directly.
Configuration();
/// Creates a [Configuration] instance from a given map.
///
/// This constructor takes a [Map] with dynamic keys and values, converts
/// all keys to strings, and then decodes the resulting map into the
/// configuration properties. This is useful when you have configuration
/// data already in a map format, possibly from a non-YAML source.
///
/// [map] The input map containing configuration data. Keys will be
/// converted to strings, while values remain as their original types.
///
/// Example:
/// ```dart
/// final configMap = {'key1': 'value1', 'key2': 42};
/// final config = MyConfiguration.fromMap(configMap);
/// ```
Configuration.fromMap(Map<dynamic, dynamic> map) {
decode(map.map<String, dynamic>((k, v) => MapEntry(k.toString(), v)));
}
/// Creates a [Configuration] instance from a YAML string.
///
/// This constructor takes a [String] containing YAML content, parses it into
/// a map, and then decodes the resulting map into the configuration properties.
/// It's useful when you have configuration data as a YAML string, perhaps
/// loaded from a file or received from an API.
///
/// [contents] A string containing valid YAML data. This will be parsed and
/// used to populate the configuration properties.
///
/// Throws a [YamlException] if the YAML parsing fails.
///
/// Example:
/// ```dart
/// final yamlString = '''
/// api_key: abc123
/// port: 8080
/// ''';
/// final config = MyConfiguration.fromString(yamlString);
/// ```
Configuration.fromString(String contents) {
final yamlMap = loadYaml(contents) as Map<dynamic, dynamic>?;
final map =
yamlMap?.map<String, dynamic>((k, v) => MapEntry(k.toString(), v));
decode(map);
}
/// Creates a [Configuration] instance from a YAML file.
///
/// [file] must contain valid YAML data.
Configuration.fromFile(File file) : this.fromString(file.readAsStringSync());
/// Returns the [ConfigurationRuntime] associated with the current instance's runtime type.
///
/// This getter retrieves the [ConfigurationRuntime] from the [RuntimeContext.current] map,
/// using the runtime type of the current instance as the key. The retrieved value
/// is then cast to [ConfigurationRuntime].
///
/// This is typically used internally to access runtime-specific configuration
/// operations and validations.
///
/// Returns:
/// The [ConfigurationRuntime] associated with this configuration's type.
///
/// Throws:
/// A runtime exception if the retrieved value cannot be cast to [ConfigurationRuntime].
ConfigurationRuntime get _runtime =>
RuntimeContext.current[runtimeType] as ConfigurationRuntime;
/// Decodes the given [value] and populates the properties of this configuration instance.
///
/// Override this method to provide decoding behavior other than the default behavior.
void decode(dynamic value) {
if (value is! Map) {
throw ConfigurationException(
this,
"input is not an object (is a '${value.runtimeType}')",
);
}
_runtime.decode(this, value);
validate();
}
/// Validates this configuration.
///
/// This method is called automatically after the configuration is decoded. It performs
/// validation checks on the configuration data to ensure its integrity and correctness.
///
/// Override this method to perform validations on input data. Throw [ConfigurationException]
/// for invalid data.
@mustCallSuper
void validate() {
_runtime.validate(this);
}
/// Retrieves an environment variable value or returns the original value.
///
/// This method checks if the given [value] is a string that starts with '$'.
/// If so, it interprets the rest of the string as an environment variable name
/// and attempts to retrieve its value from the system environment.
///
/// If the environment variable exists, its value is returned.
/// If the environment variable does not exist, null is returned.
/// If the [value] is not a string starting with '$', the original [value] is returned unchanged.
///
/// Parameters:
/// [value]: The value to check. Can be of any type.
///
/// Returns:
/// - The value of the environment variable if [value] is a string starting with '$'
/// and the corresponding environment variable exists.
/// - null if [value] is a string starting with '$' but the environment variable doesn't exist.
/// - The original [value] if it's not a string starting with '$'.
static dynamic getEnvironmentOrValue(dynamic value) {
if (value is String && value.startsWith(r"$")) {
final envKey = value.substring(1);
if (!Platform.environment.containsKey(envKey)) {
return null;
}
return Platform.environment[envKey];
}
return value;
}
}
/// An abstract class representing the runtime behavior for configuration objects.
///
/// This class provides methods for decoding and validating configuration objects,
/// as well as a utility method for handling exceptions during the decoding process.
///
/// Implementations of this class should provide concrete logic for decoding
/// configuration data from input maps and validating the resulting configuration objects.
///
/// The [tryDecode] method offers a standardized way to handle exceptions that may occur
/// during the decoding process, wrapping them in appropriate [ConfigurationException]s
/// with detailed key paths for easier debugging.
abstract class ConfigurationRuntime {
/// Decodes the input map and populates the given configuration object.
///
/// This method is responsible for parsing the input map and setting the
/// corresponding values in the configuration object. It should handle
/// type conversions, nested structures, and any specific logic required
/// for populating the configuration.
///
/// Parameters:
/// [configuration]: The Configuration object to be populated with decoded values.
/// [input]: A Map containing the raw configuration data to be decoded.
///
/// Implementations of this method should handle potential errors gracefully,
/// possibly by throwing ConfigurationException for invalid or missing data.
void decode(Configuration configuration, Map input);
/// Validates the given configuration object.
///
/// This method is responsible for performing validation checks on the
/// provided configuration object. It should ensure that all required
/// fields are present and that the values meet any specific criteria
/// or constraints defined for the configuration.
///
/// Parameters:
/// [configuration]: The Configuration object to be validated.
///
/// Implementations of this method should throw a [ConfigurationException]
/// if any validation errors are encountered, providing clear and specific
/// error messages to aid in debugging and resolution of configuration issues.
void validate(Configuration configuration);
/// Attempts to decode a configuration property and handles exceptions.
///
/// This method provides a standardized way to handle exceptions that may occur
/// during the decoding process of a configuration property. It wraps the decoding
/// logic in a try-catch block and transforms various exceptions into appropriate
/// [ConfigurationException]s with detailed key paths for easier debugging.
///
/// Parameters:
/// [configuration]: The Configuration object being decoded.
/// [name]: The name of the property being decoded.
/// [decode]: A function that performs the actual decoding logic.
///
/// Returns:
/// The result of the [decode] function if successful.
///
/// Throws:
/// [ConfigurationException]:
/// - If a [ConfigurationException] is caught, it's re-thrown with an updated key path.
/// - If an [IntermediateException] is caught, it's transformed into a [ConfigurationException]
/// with appropriate error details.
/// - For any other exception, a new [ConfigurationException] is thrown with the exception message.
///
/// This method is particularly useful for maintaining a consistent error handling
/// approach across different configuration properties and types.
dynamic tryDecode(
Configuration configuration,
String name,
dynamic Function() decode,
) {
try {
return decode();
} on ConfigurationException catch (e) {
throw ConfigurationException(
configuration,
e.message,
keyPath: [name, ...e.keyPath],
);
} on IntermediateException catch (e) {
final underlying = e.underlying;
if (underlying is ConfigurationException) {
final keyPaths = [
[name],
e.keyPath,
underlying.keyPath,
].expand((i) => i).toList();
throw ConfigurationException(
configuration,
underlying.message,
keyPath: keyPaths,
);
} else if (underlying is TypeError) {
throw ConfigurationException(
configuration,
"input is wrong type",
keyPath: [name, ...e.keyPath],
);
}
throw ConfigurationException(
configuration,
underlying.toString(),
keyPath: [name, ...e.keyPath],
);
} catch (e) {
throw ConfigurationException(
configuration,
e.toString(),
keyPath: [name],
);
}
}
}
/// Enumerates the possible options for a configuration item property's optionality.
///
/// This enum is used to specify whether a configuration property is required or optional
/// when parsing configuration data. It helps in determining how to handle missing keys
/// in the source YAML configuration.
enum ConfigurationItemAttributeType {
/// Indicates that a configuration property is required.
///
/// When a configuration property is marked as [required], it means that
/// the corresponding key must be present in the source YAML configuration.
/// If the key is missing, an exception will be thrown during the parsing
/// or validation process.
///
/// This helps ensure that all necessary configuration values are provided
/// and reduces the risk of runtime errors due to missing configuration data.
required,
/// Indicates that a configuration property is optional.
///
/// When a configuration property is marked as [optional], it means that
/// the corresponding key can be omitted from the source YAML configuration
/// without causing an error. If the key is missing, the property will be
/// silently ignored during the parsing process.
///
/// This allows for more flexible configuration structures where some
/// properties are not mandatory and can be omitted without affecting
/// the overall functionality of the configuration.
///
/// [Configuration] properties marked as [optional] will be silently ignored
/// if their source YAML doesn't contain a matching key.
optional
}
/// Represents an attribute for configuration item properties.
///
/// **NOTICE**: This will be removed in version 2.0.0.
/// To signify required or optional config you could do:
/// Example:
/// ```dart
/// class MyConfig extends Config {
/// late String required;
/// String? optional;
/// String optionalWithDefult = 'default';
/// late String optionalWithComputedDefault = _default();
///
/// String _default() => 'computed';
/// }
/// ```
class ConfigurationItemAttribute {
const ConfigurationItemAttribute._(this.type);
final ConfigurationItemAttributeType type;
}
/// A [ConfigurationItemAttribute] for required properties.
///
/// **NOTICE**: This will be removed in version 2.0.0.
/// To signify required or optional config you could do:
/// Example:
/// ```dart
/// class MyConfig extends Config {
/// late String required;
/// String? optional;
/// String optionalWithDefult = 'default';
/// late String optionalWithComputedDefault = _default();
///
/// String _default() => 'computed';
/// }
/// ```
@Deprecated("Use `late` property")
const ConfigurationItemAttribute requiredConfiguration =
ConfigurationItemAttribute._(ConfigurationItemAttributeType.required);
/// A [ConfigurationItemAttribute] for optional properties.
///
/// **NOTICE**: This will be removed in version 2.0.0.
/// To signify required or optional config you could do:
/// Example:
/// ```dart
/// class MyConfig extends Config {
/// late String required;
/// String? optional;
/// String optionalWithDefult = 'default';
/// late String optionalWithComputedDefault = _default();
///
/// String _default() => 'computed';
/// }
/// ```
@Deprecated("Use `nullable` property")
const ConfigurationItemAttribute optionalConfiguration =
ConfigurationItemAttribute._(ConfigurationItemAttributeType.optional);
/// Represents an exception thrown when reading data into a [Configuration] fails.
///
/// This exception provides detailed information about the configuration error,
/// including the configuration object where the error occurred, the error message,
/// and optionally, the key path to the problematic configuration item.
///
/// The class offers two constructors:
/// 1. A general constructor for creating exceptions with custom messages.
/// 2. A specialized constructor [ConfigurationException.missingKeys] for creating
/// exceptions specifically related to missing required keys.
///
/// The [toString] method provides a formatted error message that includes the
/// configuration type, the key path (if available), and the error message.
///
/// Usage:
/// ```dart
/// throw ConfigurationException(
/// myConfig,
/// "Invalid value",
/// keyPath: ['server', 'port'],
/// );
/// ```
///
/// Or for missing keys:
/// ```dart
/// throw ConfigurationException.missingKeys(
/// myConfig,
/// ['apiKey', 'secret'],
/// );
/// ```
class ConfigurationException {
/// Creates a new [ConfigurationException] instance.
///
/// This constructor is used to create an exception that provides information
/// about a configuration error.
///
/// Parameters:
/// - [configuration]: The [Configuration] object where the error occurred.
/// - [message]: A string describing the error.
/// - [keyPath]: An optional list of keys or indices that specify the path to
/// the problematic configuration item. Defaults to an empty list.
///
/// Example:
/// ```dart
/// throw ConfigurationException(
/// myConfig,
/// "Invalid port number",
/// keyPath: ['server', 'port'],
/// );
/// ```
ConfigurationException(
this.configuration,
this.message, {
this.keyPath = const [],
});
/// Creates a [ConfigurationException] for missing required keys.
///
/// This constructor is specifically used to create an exception when one or more
/// required keys are missing from the configuration.
///
/// Parameters:
/// - [configuration]: The [Configuration] object where the missing keys were detected.
/// - [missingKeys]: A list of strings representing the names of the missing required keys.
/// - [keyPath]: An optional list of keys or indices that specify the path to the
/// configuration item where the missing keys were expected. Defaults to an empty list.
///
/// The [message] is automatically generated to list all the missing keys.
///
/// Example:
/// ```dart
/// throw ConfigurationException.missingKeys(
/// myConfig,
/// ['apiKey', 'secret'],
/// keyPath: ['server', 'authentication'],
/// );
/// ```
ConfigurationException.missingKeys(
this.configuration,
List<String> missingKeys, {
this.keyPath = const [],
}) : message =
"missing required key(s): ${missingKeys.map((s) => "'$s'").join(", ")}";
/// The [Configuration] instance in which this exception occurred.
///
/// This field stores a reference to the [Configuration] object that was being
/// processed when the exception was thrown. It provides context about which
/// specific configuration was involved in the error, allowing for more
/// detailed error reporting and easier debugging.
///
/// The stored configuration can be used to access additional information
/// about the configuration state at the time of the error, which can be
/// helpful in diagnosing and resolving configuration-related issues.
final Configuration configuration;
/// The reason for the exception.
///
/// This field contains a string describing the specific error or reason
/// why the [ConfigurationException] was thrown. It provides detailed
/// information about what went wrong during the configuration process.
///
/// The message can be used for logging, debugging, or displaying error
/// information to users or developers to help diagnose and fix
/// configuration-related issues.
final String message;
/// The key path of the object being evaluated.
///
/// Either a string (adds '.name') or an int (adds '\[value\]').
final List<dynamic> keyPath;
/// Provides a string representation of the [ConfigurationException].
///
/// This method generates a formatted error message that includes:
/// - The type of the configuration where the error occurred
/// - The key path to the problematic configuration item (if available)
/// - The specific error message
///
/// The key path is constructed by joining the elements in [keyPath]:
/// - String elements are joined with dots (e.g., 'server.port')
/// - Integer elements are enclosed in square brackets (e.g., '[0]')
///
/// If [keyPath] is empty, a general error message for the configuration is returned.
///
/// Returns:
/// A string containing the formatted error message.
///
/// Throws:
/// [StateError] if an element in [keyPath] is neither a String nor an int.
@override
String toString() {
if (keyPath.isEmpty) {
return "Failed to read '${configuration.runtimeType}'\n\t-> $message";
}
final joinedKeyPath = StringBuffer();
for (var i = 0; i < keyPath.length; i++) {
final thisKey = keyPath[i];
if (thisKey is String) {
if (i != 0) {
joinedKeyPath.write(".");
}
joinedKeyPath.write(thisKey);
} else if (thisKey is int) {
joinedKeyPath.write("[$thisKey]");
} else {
throw StateError("not an int or String");
}
}
return "Failed to read key '$joinedKeyPath' for '${configuration.runtimeType}'\n\t-> $message";
}
}
/// Represents an error that occurs when a [Configuration] subclass is invalid and requires a change in code.
///
/// This exception is thrown when there's a structural or logical issue with a [Configuration] subclass
/// that cannot be resolved at runtime and requires modifications to the code itself.
///
/// The [ConfigurationError] provides information about the specific [Configuration] type that caused the error
/// and a descriptive message explaining the nature of the invalidity.
///
/// Properties:
/// - [type]: The Type of the [Configuration] subclass where the error occurred.
/// - [message]: A String describing the specific error or invalidity.
///
/// Usage:
/// ```dart
/// throw ConfigurationError(MyConfig, "Missing required property 'apiKey'");
/// ```
///
/// The [toString] method provides a formatted error message combining the invalid type and the error description.
class ConfigurationError {
/// Creates a new [ConfigurationError] instance.
///
/// This constructor is used to create an error that indicates an invalid [Configuration] subclass
/// which requires changes to the code itself to resolve.
///
/// Parameters:
/// - [type]: The [Type] of the [Configuration] subclass where the error occurred.
/// - [message]: A string describing the specific error or invalidity.
///
/// This error is typically thrown when there's a structural or logical issue with a [Configuration]
/// subclass that cannot be resolved at runtime and requires modifications to the code.
///
/// Example:
/// ```dart
/// throw ConfigurationError(MyConfig, "Missing required property 'apiKey'");
/// ```
ConfigurationError(this.type, this.message);
/// The type of [Configuration] in which this error appears.
///
/// This property stores the [Type] of the [Configuration] subclass that is
/// considered invalid or problematic. It provides context about which specific
/// configuration class triggered the error, allowing for more precise error
/// reporting and easier debugging.
///
/// The stored type can be used to identify the exact [Configuration] subclass
/// that needs to be modified or corrected to resolve the error.
final Type type;
/// The reason for the error.
///
/// This field contains a string describing the specific error or reason
/// why the [ConfigurationError] was thrown. It provides detailed
/// information about what makes the [Configuration] subclass invalid
/// or problematic.
///
/// The message can be used for logging, debugging, or displaying error
/// information to developers to help diagnose and fix issues related
/// to the structure or implementation of the [Configuration] subclass.
String message;
/// Returns a string representation of the [ConfigurationError].
///
/// This method generates a formatted error message that includes:
/// - The type of the invalid [Configuration] subclass
/// - The specific error message describing the invalidity
///
/// The resulting string is useful for logging, debugging, or displaying
/// error information to developers to help identify and fix issues with
/// the [Configuration] subclass implementation.
///
/// Returns:
/// A string containing the formatted error message.
@override
String toString() {
return "Invalid configuration type '$type'. $message";
}
}

View file

@ -1,303 +0,0 @@
/*
* 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_config/config.dart';
/// A [Configuration] to represent a database connection configuration.
///
/// This class extends [Configuration] and provides properties and methods
/// for managing database connection settings. It includes properties for
/// host, port, database name, username, password, and a flag for temporary
/// databases. The class supports initialization from various sources
/// (file, string, map) and provides a custom decoder for parsing connection
/// strings.
///
/// Properties:
/// - [host]: The host of the database to connect to (required).
/// - [port]: The port of the database to connect to (required).
/// - [databaseName]: The name of the database to connect to (required).
/// - [username]: A username for authenticating to the database (optional).
/// - [password]: A password for authenticating to the database (optional).
/// - [isTemporary]: A flag to represent permanence, used for test suites (optional).
///
/// The [decode] method allows parsing of connection strings or maps to
/// populate the configuration properties.
class DatabaseConfiguration extends Configuration {
/// Default constructor for DatabaseConfiguration.
///
/// Creates a new instance of DatabaseConfiguration without initializing any properties.
/// Properties can be set manually or through the decode method after instantiation.
DatabaseConfiguration();
/// Creates a [DatabaseConfiguration] instance from a file.
///
/// This named constructor initializes the configuration by reading from a file.
/// The file path is passed to the superclass constructor [Configuration.fromFile].
///
/// Parameters:
/// [file]: The path to the configuration file.
DatabaseConfiguration.fromFile(super.file) : super.fromFile();
/// Creates a [DatabaseConfiguration] instance from a YAML string.
///
/// This named constructor initializes the configuration by parsing a YAML string.
/// The YAML string is passed to the superclass constructor [Configuration.fromString].
///
/// Parameters:
/// [yaml]: A string containing YAML-formatted configuration data.
DatabaseConfiguration.fromString(super.yaml) : super.fromString();
/// Creates a [DatabaseConfiguration] instance from a Map.
///
/// This named constructor initializes the configuration using a Map of key-value pairs.
/// The Map is passed to the superclass constructor [Configuration.fromMap].
///
/// Parameters:
/// [yaml]: A Map containing configuration data.
DatabaseConfiguration.fromMap(super.yaml) : super.fromMap();
/// Creates a [DatabaseConfiguration] instance with all connection information provided.
///
/// This named constructor allows for the direct initialization of all database connection
/// properties in a single call. It sets both required and optional properties.
///
/// Parameters:
/// [username]: The username for database authentication (optional).
/// [password]: The password for database authentication (optional).
/// [host]: The host address of the database server (required).
/// [port]: The port number on which the database server is listening (required).
/// [databaseName]: The name of the specific database to connect to (required).
/// [isTemporary]: A flag indicating if this is a temporary database connection (optional, defaults to false).
///
/// This constructor provides a convenient way to create a fully configured
/// [DatabaseConfiguration] object when all connection details are known in advance.
DatabaseConfiguration.withConnectionInfo(
this.username,
this.password,
this.host,
this.port,
this.databaseName, {
this.isTemporary = false,
});
/// The host of the database to connect to.
///
/// This property represents the hostname or IP address of the database server
/// that this configuration will connect to. It is a required field and must be
/// set before attempting to establish a database connection.
///
/// The value should be a valid hostname (e.g., 'localhost', 'db.example.com')
/// or an IP address (e.g., '192.168.1.100').
///
/// This property is marked as 'late', which means it must be initialized
/// before it's first used, but not necessarily in the constructor.
late String host;
/// The port of the database to connect to.
///
/// This property represents the network port number on which the database server
/// is listening for connections. It is a required field and must be set before
/// attempting to establish a database connection.
///
/// The value should be a valid port number, typically an integer between 0 and 65535.
/// Common database port numbers include 5432 for PostgreSQL, 3306 for MySQL,
/// and 1433 for SQL Server, but the actual port may vary depending on the specific
/// database configuration.
///
/// This property is marked as 'late', which means it must be initialized
/// before it's first used, but not necessarily in the constructor.
late int port;
/// The name of the database to connect to.
///
/// This property represents the specific database name within the database server
/// that this configuration will target. It is a required field and must be set
/// before attempting to establish a database connection.
///
/// The value should be a valid database name as defined in your database server.
/// For example, it could be 'myapp_database', 'users_db', or 'production_data'.
///
/// This property is marked as 'late', which means it must be initialized
/// before it's first used, but not necessarily in the constructor.
late String databaseName;
/// A username for authenticating to the database.
///
/// This property represents the username used for authentication when connecting
/// to the database. It is an optional field, meaning it can be null if authentication
/// is not required or if other authentication methods are used.
///
/// The value should be a string containing the username as configured in the
/// database server for this particular connection. For example, it could be
/// 'db_user', 'admin', or 'app_service_account'.
///
/// If this property is set, it is typically used in conjunction with the [password]
/// property to form a complete set of credentials for database authentication.
String? username;
/// A password for authenticating to the database.
///
/// This property represents the password used for authentication when connecting
/// to the database. It is an optional field, meaning it can be null if authentication
/// is not required or if other authentication methods are used.
///
/// The value should be a string containing the password that corresponds to the
/// [username] for this database connection. For security reasons, it's important
/// to handle this value carefully and avoid exposing it in logs or user interfaces.
///
/// If this property is set, it is typically used in conjunction with the [username]
/// property to form a complete set of credentials for database authentication.
///
/// Note: In production environments, it's recommended to use secure methods of
/// storing and retrieving passwords, such as environment variables or secure
/// secret management systems, rather than hardcoding them in the configuration.
String? password;
/// A flag to represent permanence of the database.
///
/// This flag is used for test suites that use a temporary database to run tests against,
/// dropping it after the tests are complete.
/// This property is optional.
bool isTemporary = false;
/// Decodes and populates the configuration from a given value.
///
/// This method can handle two types of input:
/// 1. A Map: In this case, it delegates to the superclass's decode method.
/// 2. A String: It parses the string as a URI to extract database connection details.
///
/// For string input, it extracts:
/// - Host and port from the URI
/// - Database name from the path (if present)
/// - Username and password from the userInfo part of the URI (if present)
///
/// After parsing, it calls the validate method to ensure all required fields are set.
///
/// Parameters:
/// [value]: The input to decode. Can be a Map or a String.
///
/// Throws:
/// [ConfigurationException]: If the input is neither a Map nor a String.
@override
void decode(dynamic value) {
if (value is Map) {
super.decode(value);
return;
}
if (value is! String) {
throw ConfigurationException(
this,
"'${value.runtimeType}' is not assignable; must be a object or string",
);
}
final uri = Uri.parse(value);
host = uri.host;
port = uri.port;
if (uri.pathSegments.length == 1) {
databaseName = uri.pathSegments.first;
}
if (uri.userInfo == '') {
validate();
return;
}
final authority = uri.userInfo.split(":");
if (authority.isNotEmpty) {
username = Uri.decodeComponent(authority.first);
}
if (authority.length > 1) {
password = Uri.decodeComponent(authority.last);
}
validate();
}
}
/// A [Configuration] to represent an external HTTP API.
///
/// This class extends [Configuration] and provides properties for managing
/// external API connection settings. It includes properties for the base URL,
/// client ID, and client secret.
///
/// The class supports initialization from various sources (file, string, map)
/// through its constructors.
///
/// Properties:
/// - [baseURL]: The base URL of the described API (required).
/// - [clientID]: The client ID for API authentication (optional).
/// - [clientSecret]: The client secret for API authentication (optional).
///
/// Constructors:
/// - Default constructor: Creates an empty instance.
/// - [fromFile]: Initializes from a configuration file.
/// - [fromString]: Initializes from a YAML string.
/// - [fromMap]: Initializes from a Map.
class APIConfiguration extends Configuration {
/// Default constructor for APIConfiguration.
///
/// Creates a new instance of APIConfiguration without initializing any properties.
/// Properties can be set manually or through the decode method after instantiation.
APIConfiguration();
/// Creates an [APIConfiguration] instance from a file.
///
/// This named constructor initializes the configuration by reading from a file.
/// The file path is passed to the superclass constructor [Configuration.fromFile].
///
/// Parameters:
/// [file]: The path to the configuration file.
APIConfiguration.fromFile(super.file) : super.fromFile();
/// Creates an [APIConfiguration] instance from a YAML string.
///
/// This named constructor initializes the configuration by parsing a YAML string.
/// The YAML string is passed to the superclass constructor [Configuration.fromString].
///
/// Parameters:
/// [yaml]: A string containing YAML-formatted configuration data.
APIConfiguration.fromString(super.yaml) : super.fromString();
/// Creates an [APIConfiguration] instance from a Map.
///
/// This named constructor initializes the configuration using a Map of key-value pairs.
/// The Map is passed to the superclass constructor [Configuration.fromMap].
///
/// Parameters:
/// [yaml]: A Map containing configuration data.
APIConfiguration.fromMap(super.yaml) : super.fromMap();
/// The base URL of the described API.
///
/// This property represents the root URL for the external API that this configuration
/// is describing. It is a required field and must be set before using the API configuration.
///
/// The value should be a complete URL, including the protocol (http or https),
/// domain name, and optionally the port and base path. It serves as the foundation
/// for constructing full URLs to specific API endpoints.
///
/// This property is marked as 'late', which means it must be initialized
/// before it's first used, but not necessarily in the constructor.
///
/// This property is required.
/// Example: https://external.api.com:80/resources
late String baseURL;
/// The client ID for API authentication.
///
/// This property is optional.
String? clientID;
/// The client secret for API authentication.
///
/// This property is optional.
String? clientSecret;
}

View file

@ -1,37 +0,0 @@
/*
* 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.
*/
/// An exception class used for intermediate error handling.
///
/// This class encapsulates an underlying exception and a key path,
/// allowing for more detailed error reporting in nested structures.
///
/// [underlying] is the original exception that was caught.
/// [keyPath] is a list representing the path to the error in a nested structure.
class IntermediateException implements Exception {
/// Creates an [IntermediateException] with the given [underlying] exception and [keyPath].
///
/// [underlying] is the original exception that was caught.
/// [keyPath] is a list representing the path to the error in a nested structure.
IntermediateException(this.underlying, this.keyPath);
/// The original exception that was caught.
///
/// This field stores the underlying exception that triggered the creation
/// of this [IntermediateException]. It can be of any type, hence the
/// [dynamic] type annotation.
final dynamic underlying;
/// A list representing the path to the error in a nested structure.
///
/// This field stores the key path as a list of dynamic elements. Each element
/// in the list represents a key or index in the nested structure, helping to
/// pinpoint the exact location of the error.
final List<dynamic> keyPath;
}

View file

@ -1,504 +0,0 @@
/*
* 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:mirrors';
import 'package:protevus_config/config.dart';
/// A codec for decoding and encoding values based on their type using reflection.
///
/// This class uses the dart:mirrors library to introspect types and provide
/// appropriate decoding and encoding logic for various data types including
/// int, bool, Configuration subclasses, List, and Map.
///
/// The class supports:
/// - Decoding values from various input formats to their corresponding Dart types.
/// - Generating source code strings for decoding operations.
/// - Validating Configuration subclasses to ensure they have a default constructor.
///
/// Usage:
/// ```dart
/// final codec = MirrorTypeCodec(reflectType(SomeType));
/// final decodedValue = codec._decodeValue(inputValue);
/// final sourceCode = codec.source;
/// ```
///
/// Note: This class relies heavily on reflection, which may have performance
/// implications and is not supported in all Dart runtime environments.
class MirrorTypeCodec {
/// Constructor for MirrorTypeCodec.
///
/// This constructor takes a [TypeMirror] as its parameter and initializes the codec.
/// It performs a validation check for Configuration subclasses to ensure they have
/// a default (unnamed) constructor with all optional parameters.
///
/// Parameters:
/// [type]: The TypeMirror representing the type for which this codec is being created.
///
/// Throws:
/// [StateError]: If the type is a subclass of Configuration and doesn't have
/// an unnamed constructor with all optional parameters.
///
/// The constructor specifically:
/// 1. Checks if the type is a subclass of Configuration.
/// 2. If so, it verifies the presence of a default constructor.
/// 3. Throws a StateError if the required constructor is missing, providing
/// a detailed error message to guide the developer.
MirrorTypeCodec(this.type) {
if (type.isSubtypeOf(reflectType(Configuration))) {
final klass = type as ClassMirror;
final classHasDefaultConstructor = klass.declarations.values.any((dm) {
return dm is MethodMirror &&
dm.isConstructor &&
dm.constructorName == Symbol.empty &&
dm.parameters.every((p) => p.isOptional == true);
});
if (!classHasDefaultConstructor) {
throw StateError(
"Failed to compile '${type.reflectedType}'\n\t-> "
"'Configuration' subclasses MUST declare an unnammed constructor "
"(i.e. '${type.reflectedType}();') if they are nested.",
);
}
}
}
/// The [TypeMirror] representing the type for which this codec is created.
///
/// This field stores the reflection information about the type that this
/// [MirrorTypeCodec] instance is designed to handle. It is used throughout
/// the class to determine how to decode and encode values of this type.
final TypeMirror type;
/// Decodes a value based on its type using reflection.
///
/// This method takes a [dynamic] input value and decodes it according to the
/// type specified by this codec's [type] property. It supports decoding for:
/// - Integers
/// - Booleans
/// - Configuration subclasses
/// - Lists
/// - Maps
///
/// If the input type doesn't match any of these, the original value is returned.
///
/// Parameters:
/// [value]: The input value to be decoded.
///
/// Returns:
/// The decoded value, with its type corresponding to the codec's [type].
///
/// Throws:
/// May throw exceptions if decoding fails, particularly for nested structures
/// like Lists and Maps.
dynamic _decodeValue(dynamic value) {
if (type.isSubtypeOf(reflectType(int))) {
return _decodeInt(value);
} else if (type.isSubtypeOf(reflectType(bool))) {
return _decodeBool(value);
} else if (type.isSubtypeOf(reflectType(Configuration))) {
return _decodeConfig(value);
} else if (type.isSubtypeOf(reflectType(List))) {
return _decodeList(value as List);
} else if (type.isSubtypeOf(reflectType(Map))) {
return _decodeMap(value as Map);
}
return value;
}
/// Decodes a boolean value from various input types.
///
/// This method handles the conversion of input values to boolean:
/// - If the input is a String, it returns true if the string is "true" (case-sensitive),
/// and false otherwise.
/// - For non-String inputs, it attempts to cast the value directly to a bool.
///
/// Parameters:
/// [value]: The input value to be decoded into a boolean.
///
/// Returns:
/// A boolean representation of the input value.
///
/// Throws:
/// TypeError: If the input cannot be cast to a bool (for non-String inputs).
dynamic _decodeBool(dynamic value) {
if (value is String) {
return value == "true";
}
return value as bool;
}
/// Decodes an integer value from various input types.
///
/// This method handles the conversion of input values to integers:
/// - If the input is a String, it attempts to parse it as an integer.
/// - For non-String inputs, it attempts to cast the value directly to an int.
///
/// Parameters:
/// [value]: The input value to be decoded into an integer.
///
/// Returns:
/// An integer representation of the input value.
///
/// Throws:
/// FormatException: If the input String cannot be parsed as an integer.
/// TypeError: If the input cannot be cast to an int (for non-String inputs).
dynamic _decodeInt(dynamic value) {
if (value is String) {
return int.parse(value);
}
return value as int;
}
/// Decodes a Configuration object from the given input.
///
/// This method creates a new instance of the Configuration subclass
/// represented by this codec's type, and then decodes the input object
/// into it.
///
/// Parameters:
/// [object]: The input object to be decoded into a Configuration instance.
///
/// Returns:
/// A new instance of the Configuration subclass, populated with the decoded data.
///
/// Throws:
/// May throw exceptions if the instantiation fails or if the decode
/// method of the Configuration subclass throws an exception.
Configuration _decodeConfig(dynamic object) {
final item = (type as ClassMirror).newInstance(Symbol.empty, []).reflectee
as Configuration;
item.decode(object);
return item;
}
/// Decodes a List value based on the codec's type parameters.
///
/// This method creates a new List instance and populates it with decoded elements
/// from the input List. It uses an inner decoder to process each element according
/// to the type specified in the codec's type arguments.
///
/// Parameters:
/// [value]: The input List to be decoded.
///
/// Returns:
/// A new List containing the decoded elements.
///
/// Throws:
/// IntermediateException: If an error occurs during the decoding of any element.
/// The exception includes the index of the problematic element in its keyPath.
///
/// Note:
/// - The method creates a growable List.
/// - It uses reflection to create the new List instance.
/// - Each element is decoded using an inner decoder based on the first type argument.
List _decodeList(List value) {
final out = (type as ClassMirror).newInstance(const Symbol('empty'), [], {
const Symbol('growable'): true,
}).reflectee as List;
final innerDecoder = MirrorTypeCodec(type.typeArguments.first);
for (var i = 0; i < value.length; i++) {
try {
final v = innerDecoder._decodeValue(value[i]);
out.add(v);
} on IntermediateException catch (e) {
e.keyPath.add(i);
rethrow;
} catch (e) {
throw IntermediateException(e, [i]);
}
}
return out;
}
/// Decodes a Map value based on the codec's type parameters.
///
/// This method creates a new Map instance and populates it with decoded key-value pairs
/// from the input Map. It uses an inner decoder to process each value according
/// to the type specified in the codec's type arguments.
///
/// Parameters:
/// [value]: The input Map to be decoded.
///
/// Returns:
/// A new Map containing the decoded key-value pairs.
///
/// Throws:
/// StateError: If any key in the input Map is not a String.
/// IntermediateException: If an error occurs during the decoding of any value.
/// The exception includes the key of the problematic value in its keyPath.
///
/// Note:
/// - The method creates a new Map instance using reflection.
/// - It enforces that all keys must be Strings.
/// - Each value is decoded using an inner decoder based on the last type argument.
Map<dynamic, dynamic> _decodeMap(Map value) {
final map =
(type as ClassMirror).newInstance(Symbol.empty, []).reflectee as Map;
final innerDecoder = MirrorTypeCodec(type.typeArguments.last);
value.forEach((key, val) {
if (key is! String) {
throw StateError('cannot have non-String key');
}
try {
map[key] = innerDecoder._decodeValue(val);
} on IntermediateException catch (e) {
e.keyPath.add(key);
rethrow;
} catch (e) {
throw IntermediateException(e, [key]);
}
});
return map;
}
/// Returns a string representation of the expected type for this codec.
///
/// This getter uses the [reflectedType] property of the [type] field
/// to obtain a string representation of the type that this codec is
/// expecting to handle. This is useful for generating type-specific
/// decoding logic or for debugging purposes.
///
/// Returns:
/// A [String] representing the name of the expected type.
String get expectedType {
return type.reflectedType.toString();
}
/// Returns the source code for decoding a value based on its type.
///
/// This getter generates and returns a string containing Dart code that can be used
/// to decode a value of the type represented by this codec. The returned code varies
/// depending on the type:
///
/// - For [int], it returns code to parse integers from strings or cast to int.
/// - For [bool], it returns code to convert strings to booleans or cast to bool.
/// - For [Configuration] subclasses, it returns code to create and decode a new instance.
/// - For [List], it returns code to decode each element of the list.
/// - For [Map], it returns code to decode each value in the map.
/// - For any other type, it returns code that simply returns the input value unchanged.
///
/// The generated code assumes the input value is named 'v'.
///
/// Returns:
/// A [String] containing Dart code for decoding the value.
String get source {
if (type.isSubtypeOf(reflectType(int))) {
return _decodeIntSource;
} else if (type.isSubtypeOf(reflectType(bool))) {
return _decodeBoolSource;
} else if (type.isSubtypeOf(reflectType(Configuration))) {
return _decodeConfigSource;
} else if (type.isSubtypeOf(reflectType(List))) {
return _decodeListSource;
} else if (type.isSubtypeOf(reflectType(Map))) {
return _decodeMapSource;
}
return "return v;";
}
/// Generates source code for decoding a List value.
///
/// This getter creates a string containing Dart code that decodes a List
/// based on the codec's type parameters. The generated code:
/// - Creates a new List to store decoded elements.
/// - Defines an inner decoder function for processing each element.
/// - Iterates through the input List, decoding each element.
/// - Handles exceptions, wrapping them in IntermediateException with the index.
///
/// The decoder function uses the source code from the inner codec,
/// which is based on the first type argument of the List.
///
/// Returns:
/// A String containing the Dart code for List decoding.
String get _decodeListSource {
final typeParam = MirrorTypeCodec(type.typeArguments.first);
return """
final out = <${typeParam.expectedType}>[];
final decoder = (v) {
${typeParam.source}
};
for (var i = 0; i < (v as List).length; i++) {
try {
final innerValue = decoder(v[i]);
out.add(innerValue);
} on IntermediateException catch (e) {
e.keyPath.add(i);
rethrow;
} catch (e) {
throw IntermediateException(e, [i]);
}
}
return out;
""";
}
/// Generates source code for decoding a Map value.
///
/// This getter creates a string containing Dart code that decodes a Map
/// based on the codec's type parameters. The generated code:
/// - Creates a new Map to store decoded key-value pairs.
/// - Defines an inner decoder function for processing each value.
/// - Iterates through the input Map, ensuring all keys are Strings.
/// - Decodes each value using the inner decoder function.
/// - Handles exceptions, wrapping them in IntermediateException with the key.
///
/// The decoder function uses the source code from the inner codec,
/// which is based on the last type argument of the Map.
///
/// Returns:
/// A String containing the Dart code for Map decoding.
String get _decodeMapSource {
final typeParam = MirrorTypeCodec(type.typeArguments.last);
return """
final map = <String, ${typeParam.expectedType}>{};
final decoder = (v) {
${typeParam.source}
};
v.forEach((key, val) {
if (key is! String) {
throw StateError('cannot have non-String key');
}
try {
map[key] = decoder(val);
} on IntermediateException catch (e) {
e.keyPath.add(key);
rethrow;
} catch (e) {
throw IntermediateException(e, [key]);
}
});
return map;
""";
}
/// Generates source code for decoding a Configuration object.
///
/// This getter returns a string containing Dart code that:
/// 1. Creates a new instance of the Configuration subclass represented by [expectedType].
/// 2. Calls the `decode` method on this new instance, passing in the input value 'v'.
/// 3. Returns the decoded Configuration object.
///
/// The generated code assumes the input value is named 'v'.
///
/// Returns:
/// A [String] containing Dart code for decoding a Configuration object.
String get _decodeConfigSource {
return """
final item = $expectedType();
item.decode(v);
return item;
""";
}
/// Generates source code for decoding an integer value.
///
/// This getter returns a string containing Dart code that:
/// 1. Checks if the input value 'v' is a String.
/// 2. If it is a String, parses it to an integer using `int.parse()`.
/// 3. If it's not a String, casts the value directly to an int.
///
/// The generated code assumes the input value is named 'v'.
///
/// Returns:
/// A [String] containing Dart code for decoding an integer value.
String get _decodeIntSource {
return """
if (v is String) {
return int.parse(v);
}
return v as int;
""";
}
/// Generates source code for decoding a boolean value.
///
/// This getter returns a string containing Dart code that:
/// 1. Checks if the input value 'v' is a String.
/// 2. If it is a String, returns true if it equals "true", false otherwise.
/// 3. If it's not a String, casts the value directly to a bool.
///
/// The generated code assumes the input value is named 'v'.
///
/// Returns:
/// A [String] containing Dart code for decoding a boolean value.
String get _decodeBoolSource {
return """
if (v is String) {
return v == "true";
}
return v as bool;
""";
}
}
/// Represents a property of a Configuration class, providing metadata and decoding capabilities.
///
/// This class encapsulates information about a single property within a Configuration
/// subclass, including its name, whether it's required, and how to decode its value.
///
/// It uses the [MirrorTypeCodec] to handle the decoding of values based on the property's type.
///
/// Key features:
/// - Extracts the property name from the [VariableMirror].
/// - Determines if the property is required based on its metadata.
/// - Provides access to the decoding logic through the [codec] field.
/// - Offers a method to decode input values, taking into account environment variables.
///
/// Usage:
/// ```dart
/// final property = MirrorConfigurationProperty(someVariableMirror);
/// final decodedValue = property.decode(inputValue);
/// ```
class MirrorConfigurationProperty {
MirrorConfigurationProperty(this.property)
: codec = MirrorTypeCodec(property.type);
final VariableMirror property;
final MirrorTypeCodec codec;
String get key => MirrorSystem.getName(property.simpleName);
bool get isRequired => _isVariableRequired(property);
String get source => codec.source;
static bool _isVariableRequired(VariableMirror m) {
try {
final attribute = m.metadata
.firstWhere(
(im) =>
im.type.isSubtypeOf(reflectType(ConfigurationItemAttribute)),
)
.reflectee as ConfigurationItemAttribute;
return attribute.type == ConfigurationItemAttributeType.required;
} catch (_) {
return false;
}
}
dynamic decode(dynamic input) {
return codec._decodeValue(Configuration.getEnvironmentOrValue(input));
}
}

View file

@ -1,332 +0,0 @@
/*
* 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:mirrors';
import 'package:protevus_config/config.dart';
import 'package:protevus_runtime/runtime.dart';
/// ConfigurationRuntimeImpl is a class that extends ConfigurationRuntime and implements SourceCompiler.
///
/// This class is responsible for handling the runtime configuration of the application. It uses
/// Dart's mirror system to introspect and manipulate configuration objects at runtime.
///
/// Key features:
/// - Decodes configuration input from a Map into a strongly-typed Configuration object
/// - Validates the configuration to ensure all required fields are present
/// - Generates implementation code for decoding and validating configurations
/// - Collects and manages configuration properties
///
/// The class provides methods for decoding input, validating configurations, and compiling
/// source code for runtime configuration handling. It also includes utility methods for
/// collecting properties and generating implementation strings for decode and validate operations.
class ConfigurationRuntimeImpl extends ConfigurationRuntime
implements SourceCompiler {
/// Constructs a ConfigurationRuntimeImpl instance for the given type.
///
/// The constructor initializes the type and properties of the configuration runtime.
/// It collects properties using the `_collectProperties` method.
///
/// Parameters:
/// - type: The ClassMirror representing the type of the configuration object.
ConfigurationRuntimeImpl(this.type) {
// Should be done in the constructor so a type check could be run.
properties = _collectProperties();
}
/// The ClassMirror representing the type of the configuration object.
///
/// This field stores the reflection information for the configuration class,
/// allowing for runtime introspection and manipulation of the configuration object.
final ClassMirror type;
/// A map of property names to MirrorConfigurationProperty objects.
///
/// This late-initialized field stores the configuration properties of the class.
/// Each key is a string representing the property name, and the corresponding value
/// is a MirrorConfigurationProperty object containing metadata about that property.
///
/// The properties are collected during the initialization of the ConfigurationRuntimeImpl
/// instance and are used for decoding, validating, and generating implementation code
/// for the configuration.
late final Map<String, MirrorConfigurationProperty> properties;
/// Decodes the input map into the given configuration object.
///
/// This method takes a [Configuration] object and a [Map] input, and populates
/// the configuration object with the decoded values from the input map.
///
/// The method performs the following steps:
/// 1. Creates a copy of the input map.
/// 2. Iterates through each property in the configuration.
/// 3. For each property, it attempts to decode the corresponding value from the input.
/// 4. If the decoded value is not null and of the correct type, it sets the value on the configuration object.
/// 5. After processing all properties, it checks if there are any unexpected keys left in the input map.
///
/// Throws a [ConfigurationException] if:
/// - A decoded value is of the wrong type.
/// - There are unexpected keys in the input map after processing all known properties.
///
/// Parameters:
/// - [configuration]: The Configuration object to be populated with decoded values.
/// - [input]: A Map containing the input values to be decoded.
@override
void decode(Configuration configuration, Map input) {
final values = Map.from(input);
properties.forEach((name, property) {
final takingValue = values.remove(name);
if (takingValue == null) {
return;
}
final decodedValue = tryDecode(
configuration,
name,
() => property.decode(takingValue),
);
if (decodedValue == null) {
return;
}
if (!reflect(decodedValue).type.isAssignableTo(property.property.type)) {
throw ConfigurationException(
configuration,
"input is wrong type",
keyPath: [name],
);
}
final mirror = reflect(configuration);
mirror.setField(property.property.simpleName, decodedValue);
});
if (values.isNotEmpty) {
throw ConfigurationException(
configuration,
"unexpected keys found: ${values.keys.map((s) => "'$s'").join(", ")}.",
);
}
}
/// Generates the implementation string for the decode method.
///
/// This getter creates a String representation of the decode method implementation.
/// The generated code does the following:
/// 1. Creates a copy of the input map.
/// 2. Iterates through each property in the configuration.
/// 3. For each property:
/// - Retrieves the value from the input, considering environment variables.
/// - If a value exists, it attempts to decode it.
/// - Checks if the decoded value is of the expected type.
/// - If valid, assigns the decoded value to the configuration object.
/// 4. After processing all properties, it checks for any unexpected keys in the input.
///
/// The generated code includes proper error handling, throwing ConfigurationExceptions
/// for type mismatches or unexpected input keys.
///
/// Returns:
/// A String containing the implementation code for the decode method.
String get decodeImpl {
final buf = StringBuffer();
buf.writeln("final valuesCopy = Map.from(input);");
properties.forEach((k, v) {
buf.writeln("{");
buf.writeln(
"final v = Configuration.getEnvironmentOrValue(valuesCopy.remove('$k'));",
);
buf.writeln("if (v != null) {");
buf.writeln(
" final decodedValue = tryDecode(configuration, '$k', () { ${v.source} });",
);
buf.writeln(" if (decodedValue is! ${v.codec.expectedType}) {");
buf.writeln(
" throw ConfigurationException(configuration, 'input is wrong type', keyPath: ['$k']);",
);
buf.writeln(" }");
buf.writeln(
" (configuration as ${type.reflectedType}).$k = decodedValue as ${v.codec.expectedType};",
);
buf.writeln("}");
buf.writeln("}");
});
buf.writeln(
"""
if (valuesCopy.isNotEmpty) {
throw ConfigurationException(configuration,
"unexpected keys found: \${valuesCopy.keys.map((s) => "'\$s'").join(", ")}.");
}
""",
);
return buf.toString();
}
/// Validates the given configuration object to ensure all required properties are present.
///
/// This method performs the following steps:
/// 1. Creates a mirror of the configuration object for reflection.
/// 2. Iterates through all properties of the configuration.
/// 3. For each property, it checks if:
/// - The property is required.
/// - The property value is null or cannot be accessed.
/// 4. Collects a list of all required properties that are missing or null.
/// 5. If any required properties are missing, it throws a ConfigurationException.
///
/// Parameters:
/// - configuration: The Configuration object to be validated.
///
/// Throws:
/// - ConfigurationException: If any required properties are missing or null.
@override
void validate(Configuration configuration) {
final configMirror = reflect(configuration);
final requiredValuesThatAreMissing = properties.values
.where((v) {
try {
final value = configMirror.getField(Symbol(v.key)).reflectee;
return v.isRequired && value == null;
} catch (e) {
return true;
}
})
.map((v) => v.key)
.toList();
if (requiredValuesThatAreMissing.isNotEmpty) {
throw ConfigurationException.missingKeys(
configuration,
requiredValuesThatAreMissing,
);
}
}
/// Collects and returns a map of configuration properties for the current type.
///
/// This method traverses the class hierarchy, starting from the current type
/// up to (but not including) the Configuration class, collecting all non-static
/// and non-private variable declarations. It then creates a map where:
///
/// - Keys are the string names of the properties
/// - Values are MirrorConfigurationProperty objects created from the VariableMirrors
///
/// The method performs the following steps:
/// 1. Initializes an empty list to store VariableMirror objects.
/// 2. Traverses the class hierarchy, collecting relevant VariableMirrors.
/// 3. Creates a map from the collected VariableMirrors.
/// 4. Returns the resulting map of property names to MirrorConfigurationProperty objects.
///
/// Returns:
/// A Map<String, MirrorConfigurationProperty> representing the configuration properties.
Map<String, MirrorConfigurationProperty> _collectProperties() {
final declarations = <VariableMirror>[];
var ptr = type;
while (ptr.isSubclassOf(reflectClass(Configuration))) {
declarations.addAll(
ptr.declarations.values
.whereType<VariableMirror>()
.where((vm) => !vm.isStatic && !vm.isPrivate),
);
ptr = ptr.superclass!;
}
final m = <String, MirrorConfigurationProperty>{};
for (final vm in declarations) {
final name = MirrorSystem.getName(vm.simpleName);
m[name] = MirrorConfigurationProperty(vm);
}
return m;
}
/// Generates the implementation string for the validate method.
///
/// This getter creates a String representation of the validate method implementation.
/// The generated code does the following:
/// 1. Initializes a list to store missing keys.
/// 2. Iterates through each property in the configuration.
/// 3. For each property:
/// - Attempts to retrieve the property value from the configuration object.
/// - Checks if the property is required and its value is null.
/// - If required and null, or if an error occurs during retrieval, adds the property name to the missing keys list.
/// 4. After checking all properties, throws a ConfigurationException if any keys are missing.
///
/// The generated code includes error handling to catch any issues during property access.
///
/// Returns:
/// A String containing the implementation code for the validate method.
String get validateImpl {
final buf = StringBuffer();
const startValidation = """
final missingKeys = <String>[];
""";
buf.writeln(startValidation);
properties.forEach((name, property) {
final propCheck = """
try {
final $name = (configuration as ${type.reflectedType}).$name;
if (${property.isRequired} && $name == null) {
missingKeys.add('$name');
}
} on Error catch (e) {
missingKeys.add('$name');
}""";
buf.writeln(propCheck);
});
const throwIfErrors = """
if (missingKeys.isNotEmpty) {
throw ConfigurationException.missingKeys(configuration, missingKeys);
}""";
buf.writeln(throwIfErrors);
return buf.toString();
}
/// Compiles the configuration runtime implementation into a string representation.
///
/// This method generates the source code for a ConfigurationRuntimeImpl class
/// that extends ConfigurationRuntime. The generated class includes implementations
/// for the 'decode' and 'validate' methods.
///
/// The method performs the following steps:
/// 1. Retrieves import directives for the current type and adds them to the generated code.
/// 2. Adds an import for the intermediate_exception.dart file.
/// 3. Creates an instance of ConfigurationRuntimeImpl.
/// 4. Generates the class definition with implementations of decode and validate methods.
///
/// Parameters:
/// - ctx: A BuildContext object used to retrieve import directives.
///
/// Returns:
/// A Future<String> containing the generated source code for the ConfigurationRuntimeImpl class.
@override
Future<String> compile(BuildContext ctx) async {
final directives = await ctx.getImportDirectives(
uri: type.originalDeclaration.location!.sourceUri,
alsoImportOriginalFile: true,
)
..add("import 'package:conduit_config/src/intermediate_exception.dart';");
return """
${directives.join("\n")}
final instance = ConfigurationRuntimeImpl();
class ConfigurationRuntimeImpl extends ConfigurationRuntime {
@override
void decode(Configuration configuration, Map input) {
$decodeImpl
}
@override
void validate(Configuration configuration) {
$validateImpl
}
}
""";
}
}

View file

@ -1,19 +0,0 @@
name: protevus_config
description: The Configuration Package for the Protevus Platform
version: 0.0.1
homepage: https://protevus.com
documentation: https://docs.protevus.com
repository: https://git.protevus.com/protevus/platform
environment:
sdk: ^3.4.2
# Add regular dependencies here.
dependencies:
protevus_runtime: ^0.0.1
meta: ^1.3.0
yaml: ^3.1.2
dev_dependencies:
lints: ^3.0.0
test: ^1.24.0

View file

@ -1,7 +0,0 @@
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/
# Avoid committing pubspec.lock for library packages; see
# https://dart.dev/guides/libraries/private-files#pubspeclock.
pubspec.lock

View file

@ -1,3 +0,0 @@
## 1.0.0
- Initial version.

View file

@ -1,10 +0,0 @@
The MIT License (MIT)
The Laravel Framework is Copyright (c) Taylor Otwell
The Fabric Framework is Copyright (c) Vieo, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1 +0,0 @@
<p align="center"><a href="https://protevus.com" target="_blank"><img src="https://git.protevus.com/protevus/branding/raw/branch/main/protevus-logo-bg.png"></a></p>

View file

@ -1,30 +0,0 @@
# This file configures the static analysis results for your project (errors,
# warnings, and lints).
#
# This enables the 'recommended' set of lints from `package:lints`.
# This set helps identify many issues that may lead to problems when running
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
# style and format.
#
# If you want a smaller set of lints you can change this to specify
# 'package:lints/core.yaml'. These are just the most critical lints
# (the recommended set includes the core lints).
# The core lints are also what is used by pub.dev for scoring packages.
include: package:lints/recommended.yaml
# Uncomment the following section to specify additional rules.
# linter:
# rules:
# - camel_case_types
# analyzer:
# exclude:
# - path/to/excluded/files/**
# For more information about the core and recommended set of lints, see
# https://dart.dev/go/core-lints
# For additional information about configuring this file, see
# https://dart.dev/guides/language/analysis-options

View file

@ -1,25 +0,0 @@
/*
* 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';
export 'src/schema/schema.dart';

View file

@ -1,319 +0,0 @@
/*
* 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';
/// Annotation to configure the table definition of a [ManagedObject].
///
/// Adding this metadata to a table definition (`T` in `ManagedObject<T>`) configures the behavior of the underlying table.
/// For example:
///
/// class User extends ManagedObject<_User> implements _User {}
///
/// @Table(name: "_Account");
/// class _User {
/// @primaryKey
/// int id;
///
/// String name;
/// String email;
/// }
class Table {
/// Default constructor.
///
/// If [name] is provided, the name of the underlying table will be its value. Otherwise,
/// the name of the underlying table matches the name of the table definition class.
///
/// See also [Table.unique] for the behavior of [uniquePropertySet].
const Table({
this.useSnakeCaseName = false,
this.name,
this.uniquePropertySet,
this.useSnakeCaseColumnName = false,
});
/// Configures each instance of a table definition to be unique for the combination of [properties].
///
/// Adding this metadata to a table definition requires that all instances of this type
/// must be unique for the combined properties in [properties]. [properties] must contain symbolic names of
/// properties declared in the table definition, and those properties must be either attributes
/// or belongs-to relationship properties. See [Table] for example.
const Table.unique(List<Symbol> properties)
: this(uniquePropertySet: properties);
/// Each instance of the associated table definition is unique for these properties.
///
/// null if not set.
final List<Symbol>? uniquePropertySet;
/// Useful to indicate using new snake_case naming convention if [name] is not set
/// This property defaults to false to avoid breaking change ensuring backward compatibility
final bool useSnakeCaseName;
/// The name of the underlying database table.
///
/// If this value is not set, the name defaults to the name of the table definition class using snake_case naming convention without the prefix '_' underscore.
final String? name;
/// Useful to indicate using new snake_case naming convention for columns.
/// This property defaults to false to avoid breaking change ensuring backward compatibility
///
/// If a column is annotated with `@Column()` with a non-`null` value for
/// `name` or `useSnakeCaseName`, that value takes precedent.
final bool useSnakeCaseColumnName;
}
/// Possible values for a delete rule in a [Relate].
enum DeleteRule {
/// Prevents a delete operation if the would-be deleted [ManagedObject] still has references to this relationship.
restrict,
/// All objects with a foreign key reference to the deleted object will also be deleted.
cascade,
/// All objects with a foreign key reference to the deleted object will have that reference nullified.
nullify,
/// All objects with a foreign key reference to the deleted object will have that reference set to the column's default value.
setDefault
}
/// Metadata to configure property of [ManagedObject] as a foreign key column.
///
/// A property in a [ManagedObject]'s table definition with this metadata will map to a database column
/// that has a foreign key reference to the related [ManagedObject]. Relationships are made up of two [ManagedObject]s, where each
/// has a property that refers to the other. Only one of those properties may have this metadata. The property with this metadata
/// resolves to a column in the database. The relationship property without this metadata resolves to a row or rows in the database.
class Relate {
/// Creates an instance of this type.
const Relate(
this.inversePropertyName, {
this.onDelete = DeleteRule.nullify,
this.isRequired = false,
});
const Relate.deferred(DeleteRule onDelete, {bool isRequired = false})
: this(_deferredSymbol, onDelete: onDelete, isRequired: isRequired);
/// The symbol for the property in the related [ManagedObject].
///
/// This value must be the symbol for the property in the related [ManagedObject]. This creates the link between
/// two sides of a relationship between a [ManagedObject].
final Symbol inversePropertyName;
/// The delete rule to use when a related instance is deleted.
///
/// This rule dictates how the database should handle deleting objects that have relationships. See [DeleteRule] for possible options.
///
/// If [isRequired] is true, this value may not be [DeleteRule.nullify]. This value defaults to [DeleteRule.nullify].
final DeleteRule onDelete;
/// Whether or not this relationship is required.
///
/// By default, [Relate] properties are not required to support the default value of [onDelete].
/// By setting this value to true, an instance of this entity cannot be created without a valid value for the relationship property.
final bool isRequired;
bool get isDeferred => inversePropertyName == _deferredSymbol;
static const Symbol _deferredSymbol = #mdrDeferred;
}
/// Metadata to describe the behavior of the underlying database column of a persistent property in [ManagedObject] subclasses.
///
/// By default, declaring a property in a table definition will make it a database column
/// and its database column will be derived from the property's type.
/// If the property needs additional directives - like indexing or uniqueness - it should be annotated with an instance of this class.
///
/// class User extends ManagedObject<_User> implements _User {}
/// class _User {
/// @primaryKey
/// int id;
///
/// @Column(indexed: true, unique: true)
/// String email;
/// }
class Column {
/// Creates an instance of this type.
///
/// [defaultValue] is sent as-is to the database, therefore, if the default value is the integer value 2,
/// pass the string "2". If the default value is a string, it must also be wrapped in single quotes: "'defaultValue'".
const Column({
this.databaseType,
bool primaryKey = false,
bool nullable = false,
this.defaultValue,
bool unique = false,
bool indexed = false,
bool omitByDefault = false,
this.autoincrement = false,
this.validators = const [],
this.useSnakeCaseName,
this.name,
}) : isPrimaryKey = primaryKey,
isNullable = nullable,
isUnique = unique,
isIndexed = indexed,
shouldOmitByDefault = omitByDefault;
/// When true, indicates that this property is the primary key.
///
/// Only one property of a class may have primaryKey equal to true.
final bool isPrimaryKey;
/// The type of the field in the database.
///
/// By default, the database column type is inferred from the Dart type of the property, e.g. a Dart [String] is a PostgreSQL text type.
/// This allows you to override the default type mapping for the annotated property.
final ManagedPropertyType? databaseType;
/// Indicates whether or not the property can be null or not.
///
/// By default, properties are not nullable.
final bool isNullable;
/// The default value of the property.
///
/// By default, a property does not have a default property. This is a String to be interpreted by the database driver. For example,
/// a PostgreSQL datetime column that defaults to the current time:
///
/// class User extends ManagedObject<_User> implements _User {}
/// class _User {
/// @Column(defaultValue: "now()")
/// DateTime createdDate;
///
/// ...
/// }
final String? defaultValue;
/// Whether or not the property is unique among all instances.
///
/// By default, properties are not unique.
final bool isUnique;
/// Whether or not the backing database should generate an index for this property.
///
/// By default, properties are not indexed. Properties that are used often in database queries should be indexed.
final bool isIndexed;
/// Whether or not fetching an instance of this type should include this property.
///
/// By default, all properties on a [ManagedObject] are returned if not specified (unless they are has-one or has-many relationship properties).
/// This flag will remove the associated property from the result set unless it is explicitly specified by [Query.returningProperties].
final bool shouldOmitByDefault;
/// A sequence generator will be used to generate the next value for this column when a row is inserted.
///
/// When this flag is true, the database will generate a value for this column on insert.
final bool autoincrement;
/// A list of validators to apply to the annotated property.
///
/// Validators in this list will be applied to the annotated property.
///
/// When the data model is compiled, this list is combined with any `Validate` annotations on the annotated property.
///
final List<Validate> validators;
/// Useful to indicate using new snake_case naming convention if [name] is not set
///
/// This property defaults to null to delegate to [Table.useSnakeCaseColumnName]
/// The default value, `null`, indicates that the behavior should be
/// acquired from the [Table.useSnakeCaseColumnName] annotation on the
/// enclosing class.
final bool? useSnakeCaseName;
/// The name of the underlying column in table.
///
/// If this value is not set, the name defaults to the name of the model attribute using snake_case naming convention.
final String? name;
}
/// An annotation used to specify how a Model is serialized in API responses.
@Target({TargetKind.classType})
class ResponseModel {
const ResponseModel({this.includeIfNullField = true});
/// Whether the serializer should include fields with `null` values in the
/// serialized Model output.
///
/// If `true` (the default), all fields in the Model are written to JSON, even if they are
/// `null`.
///
/// If a field is annotated with `@ResponseKey()` with a non-`null` value for
/// `includeIfNull`, that value takes precedent.
final bool includeIfNullField;
}
/// An annotation used to specify how a field is serialized in API responses.
@Target({TargetKind.field, TargetKind.getter, TargetKind.setter})
class ResponseKey {
const ResponseKey({this.name, this.includeIfNull});
/// The name to be used when serializing this field.
///
/// If this value is not set, the name defaults to [Column.name].
final String? name;
/// Whether the serializer should include the field with `null` value in the
/// serialized output.
///
/// If `true`, the serializer should include the field in the serialized
/// output, even if the value is `null`.
///
/// The default value, `null`, indicates that the behavior should be
/// acquired from the [ResponseModel.includeIfNullField] annotation on the
/// enclosing class.
final bool? includeIfNull;
}
/// Annotation for [ManagedObject] properties that allows them to participate in [ManagedObject.asMap] and/or [ManagedObject.readFromMap].
///
/// See constructor.
class Serialize {
/// Annotates a [ManagedObject] property so it can be serialized.
///
/// A [ManagedObject] property declaration with this metadata will have its value encoded/decoded when
/// converting the managed object to and from a [Map].
///
/// If [input] is true, this property's value is set when converting from a map.
///
/// If [output] is true, this property is in the map created by [ManagedObject.asMap].
/// This key is only included if the value is non-null.
///
/// Both [input] and [output] default to true.
const Serialize({bool input = true, bool output = true})
: isAvailableAsInput = input,
isAvailableAsOutput = output;
/// See constructor.
final bool isAvailableAsInput;
/// See constructor.
final bool isAvailableAsOutput;
}
/// Primary key annotation for a ManagedObject table definition property.
///
/// This annotation is a convenience for the following annotation:
///
/// @Column(primaryKey: true, databaseType: ManagedPropertyType.bigInteger, autoincrement: true)
/// int id;
///
/// The annotated property type must be [int].
///
/// The validator [Validate.constant] is automatically applied to a property with this annotation.
const Column primaryKey = Column(
primaryKey: true,
databaseType: ManagedPropertyType.bigInteger,
autoincrement: true,
validators: [Validate.constant()],
);

View file

@ -1,279 +0,0 @@
/*
* 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. "
"This prohibits 'ManagedObject' and 'ManagedSet' properties, except for 'ManagedObject' "
"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 = {};
@override
dynamic valueForProperty(ManagedPropertyDescription property) {
return contents[property.name];
}
@override
void setValueForProperty(ManagedPropertyDescription property, dynamic value) {
if (value != null) {
if (!property.isAssignableWith(value)) {
throw ValidationException(
["invalid input value for '${property.name}'"],
);
}
}
contents[property.name] = value;
}
}
/// 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(
ManagedEntity entity,
ManagedBacking backing,
) {
if (backing.contents.containsKey(entity.primaryKey)) {
contents[entity.primaryKey] = backing.contents[entity.primaryKey];
}
}
@override
Map<String, dynamic> contents = {};
@override
dynamic valueForProperty(ManagedPropertyDescription property) {
if (property is ManagedAttributeDescription && property.isPrimaryKey) {
return contents[property.name];
}
throw _invalidValueConstruction;
}
@override
void setValueForProperty(ManagedPropertyDescription property, dynamic value) {
if (property is ManagedAttributeDescription && property.isPrimaryKey) {
contents[property.name] = value;
return;
}
throw _invalidValueConstruction;
}
}
/// 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) {
if (original is! ManagedValueBacking) {
throw ArgumentError(
"Invalid 'ManagedObject' assignment to 'Query.values'. Object must be created through default constructor.",
);
}
original.contents.forEach((key, value) {
final prop = entity.properties[key];
if (prop == null) {
throw ArgumentError(
"Invalid 'ManagedObject' assignment to 'Query.values'. Property '$key' does not exist for '${entity.name}'.",
);
}
if (prop is ManagedRelationshipDescription) {
if (!prop.isBelongsTo) {
return;
}
}
setValueForProperty(prop, value);
});
}
/// 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) {
if (!property.isBelongsTo) {
throw _invalidValueConstruction;
}
if (!contents.containsKey(property.name)) {
contents[property.name] = property.inverse!.entity
.instanceOf(backing: ManagedForeignKeyBuilderBacking());
}
}
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) {
if (!property.isBelongsTo) {
throw _invalidValueConstruction;
}
if (value == null) {
contents[property.name] = null;
} else {
final original = value as ManagedObject;
final replacementBacking = ManagedForeignKeyBuilderBacking.from(
original.entity,
original.backing,
);
final replacement =
original.entity.instanceOf(backing: replacementBacking);
contents[property.name] = replacement;
}
} else {
contents[property.name] = value;
}
}
}
/// 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;
@override
Map<String, dynamic> get contents => {};
@override
dynamic valueForProperty(ManagedPropertyDescription property) {
if (workingKeyPath != null) {
workingKeyPath!.add(property);
return forward(property, workingKeyPath);
}
final keyPath = KeyPath(property);
keyPaths.add(keyPath);
return forward(property, keyPath);
}
@override
void setValueForProperty(ManagedPropertyDescription property, dynamic value) {
// no-op
}
dynamic forward(ManagedPropertyDescription property, KeyPath? keyPath) {
if (property is ManagedRelationshipDescription) {
final tracker = ManagedAccessTrackingBacking()..workingKeyPath = keyPath;
if (property.relationshipType == ManagedRelationshipType.hasMany) {
return property.inverse!.entity.setOf([]);
} else {
return property.destinationEntity.instanceOf(backing: tracker);
}
} else if (property is ManagedAttributeDescription &&
property.type!.kind == ManagedPropertyType.document) {
return DocumentAccessTracker(keyPath);
}
return null;
}
}
/// 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);
final KeyPath? owner;
@override
dynamic operator [](dynamic keyOrIndex) {
owner!.addDynamicElement(keyOrIndex);
return this;
}
}

View file

@ -1,263 +0,0 @@
/*
* 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';
import 'package:protevus_database/src/persistent_store/persistent_store.dart';
import 'package:protevus_database/src/query/query.dart';
/// A service object that handles connecting to and sending queries to a database.
///
/// You create objects of this type to use the Conduit ORM. Create instances in [ApplicationChannel.prepare]
/// and inject them into controllers that execute database queries.
///
/// A context contains two types of objects:
///
/// - [PersistentStore] : Maintains a connection to a specific database. Transfers data between your application and the database.
/// - [ManagedDataModel] : Contains information about the [ManagedObject] subclasses in your application.
///
/// Example usage:
///
/// class Channel extends ApplicationChannel {
/// ManagedContext context;
///
/// @override
/// Future prepare() async {
/// var store = new PostgreSQLPersistentStore(...);
/// var dataModel = new ManagedDataModel.fromCurrentMirrorSystem();
/// context = new ManagedContext(dataModel, store);
/// }
///
/// @override
/// Controller get entryPoint {
/// final router = new Router();
/// router.route("/path").link(() => new DBController(context));
/// return router;
/// }
/// }
class ManagedContext implements APIComponentDocumenter {
/// Creates a new instance of [ManagedContext] with the provided [dataModel] and [persistentStore].
///
/// This is the default constructor.
///
/// A [Query] is sent to the database described by [persistentStore]. A [Query] may only be executed
/// on this context if its type is in [dataModel].
ManagedContext(this.dataModel, this.persistentStore) {
mm.add(dataModel!);
_finalizer.attach(this, persistentStore, detach: this);
}
/// 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.
final ManagedDataModel? dataModel;
/// Runs all [Query]s in [transactionBlock] within a database transaction.
///
/// Queries executed within [transactionBlock] will be executed as a database transaction.
/// A [transactionBlock] is passed a [ManagedContext] that must be the target of all queries
/// within the block. The context passed to the [transactionBlock] is *not* the same as
/// the context the transaction was created from.
///
/// *You must not use the context this method was invoked on inside the transactionBlock.
/// Doing so will deadlock your application.*
///
/// If an exception is encountered in [transactionBlock], any query that has already been
/// executed will be rolled back and this method will rethrow the exception.
///
/// You may manually rollback a query by throwing a [Rollback] object. This will exit the
/// [transactionBlock], roll back any changes made in the transaction, but this method will not
/// throw.
///
/// Rollback takes a string but the transaction
/// 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
/// The parameter passed to [Rollback]'s constructor will be returned from this method
/// so that the caller can determine why the transaction was rolled back.
///
/// Example usage:
///
/// await context.transaction((transaction) async {
/// final q = new Query<Model>(transaction)
/// ..values = someObject;
/// await q.insert();
/// ...
/// });
Future<T> transaction<T>(
Future<T> Function(ManagedContext transaction) transactionBlock,
) {
return persistentStore.transaction(
ManagedContext.childOf(this),
transactionBlock,
);
}
/// 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.
Future close() async {
await persistentStore.close();
_finalizer.detach(this);
mm.remove(dataModel!);
}
/// Returns the [ManagedEntity] for the given [type] from the [dataModel].
///
/// See [ManagedDataModel.entityForType].
ManagedEntity entityForType(Type type) {
return dataModel!.entityForType(type);
}
/// Inserts a single [object] into this context.
///
/// 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();
}
/// Inserts each object in [objects] into this context.
///
/// 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 {
return Query<T>(this).insertMany(objects);
}
/// Returns an object of type [T] from this context if it exists, otherwise returns null.
///
/// 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 {
final entity = dataModel!.tryEntityForType(T);
if (entity == null) {
throw ArgumentError("Unknown entity '$T' in fetchObjectWithID. "
"Provide a type to this method and ensure it is in this context's data model.");
}
final primaryKey = entity.primaryKeyAttribute!;
if (!primaryKey.type!.isAssignableWith(identifier)) {
return null;
}
final query = Query<T>(this)
..where((o) => o[primaryKey.name]).equalTo(identifier);
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);
}
/// 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.
class Rollback {
/// Default constructor, takes a [reason] object that can be anything.
///
/// The parameter [reason] will be returned by [ManagedContext.transaction].
Rollback(this.reason);
/// The reason this rollback occurred.
///
/// This value is returned from [ManagedContext.transaction] when this instance is thrown.
final String reason;
}

View file

@ -1,163 +0,0 @@
/*
* 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';
import 'package:protevus_database/src/query/query.dart';
import 'package:protevus_runtime/runtime.dart';
/// Instances of this class contain descriptions and metadata for mapping [ManagedObject]s to database rows.
///
/// An instance of this type must be used to initialize a [ManagedContext], and so are required to use [Query]s.
///
/// The [ManagedDataModel.fromCurrentMirrorSystem] constructor will reflect on an application's code and find
/// 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.
///
/// To register a class as a managed object within this data model, you must include its type in the list. Example:
///
/// new DataModel([User, Token, Post]);
ManagedDataModel(List<Type> instanceTypes) {
final runtimes = RuntimeContext.current.runtimes.iterable
.whereType<ManagedEntityRuntime>()
.toList();
final expectedRuntimes = instanceTypes
.map(
(t) => runtimes.firstWhereOrNull((e) => e.entity.instanceType == t),
)
.toList();
if (expectedRuntimes.any((e) => e == null)) {
throw ManagedDataModelError(
"Data model types were not found!",
);
}
for (final runtime in expectedRuntimes) {
_entities[runtime!.entity.instanceType] = runtime.entity;
_tableDefinitionToEntityMap[runtime.entity.tableDefinition] =
runtime.entity;
}
for (final runtime in expectedRuntimes) {
runtime!.finalize(this);
}
}
/// Creates an instance of a [ManagedDataModel] from all subclasses of [ManagedObject] in all libraries visible to the calling library.
///
/// This constructor will search every available package and file library that is visible to the library
/// that runs this constructor for subclasses of [ManagedObject]. A [ManagedEntity] will be created
/// and stored in this instance for every such class found.
///
/// Standard Dart libraries (prefixed with 'dart:') and URL-encoded libraries (prefixed with 'data:') are not searched.
///
/// This is the preferred method of instantiating this type.
ManagedDataModel.fromCurrentMirrorSystem() {
final runtimes = RuntimeContext.current.runtimes.iterable
.whereType<ManagedEntityRuntime>();
for (final runtime in runtimes) {
_entities[runtime.entity.instanceType] = runtime.entity;
_tableDefinitionToEntityMap[runtime.entity.tableDefinition] =
runtime.entity;
}
for (final runtime in runtimes) {
runtime.finalize(this);
}
}
/// 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].
///
/// [type] may be either a subclass of [ManagedObject] or a [ManagedObject]'s table definition. For example, the following
/// definition, you could retrieve its entity by passing MyModel or _MyModel as an argument to this method:
///
/// class MyModel extends ManagedObject<_MyModel> implements _MyModel {}
/// class _MyModel {
/// @primaryKey
/// int id;
/// }
/// If the [type] has no known [ManagedEntity] then a [StateError] is thrown.
/// Use [tryEntityForType] to test if an entity exists.
ManagedEntity entityForType(Type type) {
final entity = tryEntityForType(type);
if (entity == null) {
throw StateError(
"No entity found for '$type. Did you forget to create a 'ManagedContext'?",
);
}
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) {
e.documentComponents(context);
}
}
}
/// 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);
final String message;
@override
String toString() {
return "Data Model Error: $message";
}
}

View file

@ -1,77 +0,0 @@
/*
* 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,
}) {
for (final d in _dataModels.keys) {
final entity = d.tryEntityForType(type);
if (entity != null) {
return entity;
}
}
if (orElse == null) {
throw StateError(
"No entity found for '$type. Did you forget to create a 'ManagedContext'?",
);
}
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);
if (_dataModels[dataModel]! < 1) {
_dataModels.remove(dataModel);
}
}
}

View file

@ -1,66 +0,0 @@
/*
* 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.
///
/// [Document]s may be properties of [ManagedObject] table definition. They are a container
/// for [data] that is a JSON-encodable [Map] or [List]. When storing a [Document] in a database column,
/// [data] is JSON-encoded.
///
/// Use this type to store unstructured or 'schema-less' data. Example:
///
/// class Event extends ManagedObject<_Event> implements _Event {}
/// class _Event {
/// @primaryKey
/// int id;
///
/// String type;
///
/// Document details;
/// }
class Document {
/// Creates an instance with an optional initial [data].
///
/// If no argument is passed, [data] is null. Otherwise, it is the first argument.
Document([this.data]);
/// The JSON-encodable data contained by this instance.
///
/// This value must be JSON-encodable.
dynamic data;
/// Returns an element of [data] by index or key.
///
/// [keyOrIndex] may be a [String] or [int].
///
/// When [data] is a [Map], [keyOrIndex] must be a [String] and will return the object for the key
/// in that map.
///
/// When [data] is a [List], [keyOrIndex] must be a [int] and will return the object at the index
/// in that list.
dynamic operator [](Object keyOrIndex) {
return data[keyOrIndex];
}
/// Sets an element of [data] by index or key.
///
/// [keyOrIndex] may be a [String] or [int]. [value] must be a JSON-encodable value.
///
/// When [data] is a [Map], [keyOrIndex] must be a [String] and will set [value] for the key
/// [keyOrIndex].
///
/// When [data] is a [List], [keyOrIndex] must be a [int] and will set [value] for the index
/// [keyOrIndex]. This index must be within the length of [data]. For all other [List] operations,
/// you may cast [data] to [List].
void operator []=(Object keyOrIndex, dynamic value) {
data[keyOrIndex] = value;
}
}

View file

@ -1,580 +0,0 @@
/*
* 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';
import 'package:protevus_database/src/managed/relationship_type.dart';
import 'package:protevus_database/src/query/query.dart';
import 'package:protevus_openapi/v3.dart';
import 'package:protevus_runtime/runtime.dart';
/// Mapping information between a table in a database and a [ManagedObject] object.
///
/// An entity defines the mapping between a database table and [ManagedObject] subclass. Entities
/// are created by declaring [ManagedObject] subclasses and instantiating a [ManagedDataModel].
/// In general, you do not need to use or create instances of this class.
///
/// An entity describes the properties that a subclass of [ManagedObject] will have and their representation in the underlying database.
/// Each of these properties are represented by an instance of a [ManagedPropertyDescription] subclass. A property is either an attribute or a relationship.
///
/// Attribute values are scalar (see [ManagedPropertyType]) - [int], [String], [DateTime], [double] and [bool].
/// Attributes are typically backed by a column in the underlying database for a [ManagedObject], but may also represent transient values
/// defined by the [instanceType].
/// Attributes are represented by [ManagedAttributeDescription].
///
/// The value of a relationship property is a reference to another [ManagedObject]. If a relationship property has [Relate] metadata,
/// the property is backed be a foreign key column in the underlying database. Relationships are represented by [ManagedRelationshipDescription].
class ManagedEntity implements APIComponentDocumenter {
/// Creates an instance of this type..
///
/// You should never call this method directly, it will be called by [ManagedDataModel].
ManagedEntity(this._tableName, this.instanceType, this.tableDefinition);
/// The name of this entity.
///
/// This name will match the name of [instanceType].
String get name => instanceType.toString();
/// The type of instances represented by this entity.
///
/// Managed objects are made up of two components, a table definition and an instance type. Applications
/// use instances of the instance type to work with queries and data from the database table this entity represents.
final Type instanceType;
/// Set of callbacks that are implemented differently depending on compilation target.
///
/// If running in default mode (mirrors enabled), is a set of mirror operations. Otherwise,
/// code generated.
ManagedEntityRuntime get runtime =>
RuntimeContext.current[instanceType] as ManagedEntityRuntime;
/// The name of type of persistent instances represented by this entity.
///
/// Managed objects are made up of two components, a table definition and an instance type.
/// The system uses this type to define the mapping to the underlying database table.
final String tableDefinition;
/// All attribute values of this entity.
///
/// An attribute maps to a single column or field in a database that is a scalar value, such as a string, integer, etc. or a
/// transient property declared in the instance type.
/// The keys are the case-sensitive name of the attribute. Values that represent a relationship to another object
/// are not stored in [attributes].
late Map<String, ManagedAttributeDescription?> attributes;
/// All relationship values of this entity.
///
/// A relationship represents a value that is another [ManagedObject] or [ManagedSet] of [ManagedObject]s. Not all relationships
/// correspond to a column or field in a database, only those with [Relate] metadata (see also [ManagedRelationshipType.belongsTo]). In
/// this case, the underlying database column is a foreign key reference. The underlying database does not have storage
/// for [ManagedRelationshipType.hasMany] or [ManagedRelationshipType.hasOne] properties, as those values are derived by the foreign key reference
/// on the inverse relationship property.
/// Keys are the case-sensitive name of the relationship.
late Map<String, ManagedRelationshipDescription?> relationships;
/// All properties (relationships and attributes) of this entity.
///
/// The string key is the name of the property, case-sensitive. Values will be instances of either [ManagedAttributeDescription]
/// or [ManagedRelationshipDescription]. This is the concatenation of [attributes] and [relationships].
Map<String, ManagedPropertyDescription?> get properties {
final all = Map<String, ManagedPropertyDescription?>.from(attributes);
all.addAll(relationships);
return all;
}
/// Set of properties that, together, are unique for each instance of this entity.
///
/// If non-null, each instance of this entity is unique for the combination of values
/// for these properties. Instances may have the same values for each property in [uniquePropertySet],
/// but cannot have the same value for all properties in [uniquePropertySet]. This differs from setting
/// a single property as unique with [Column], where each instance has
/// a unique value for that property.
///
/// This value is set by adding [Table] to the table definition of a [ManagedObject].
List<ManagedPropertyDescription>? uniquePropertySet;
/// List of [ManagedValidator]s for attributes of this entity.
///
/// All validators for all [attributes] in one, flat list. Order is undefined.
late List<ManagedValidator> validators;
/// 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>? get defaultProperties {
if (_defaultProperties == null) {
final elements = <String?>[];
elements.addAll(
attributes.values
.where((prop) => prop!.isIncludedInDefaultResultSet)
.where((prop) => !prop!.isTransient)
.map((prop) => prop!.name),
);
elements.addAll(
relationships.values
.where(
(prop) =>
prop!.isIncludedInDefaultResultSet &&
prop.relationshipType == ManagedRelationshipType.belongsTo,
)
.map((prop) => prop!.name),
);
_defaultProperties = List.unmodifiable(elements);
}
return _defaultProperties;
}
/// Name of primary key property.
///
/// 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];
}
/// A map from accessor symbol name to property name.
///
/// This map should not be modified.
late Map<Symbol, String> symbolMap;
/// Name of table in database 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.
String get tableName {
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 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.
T instanceOf<T extends ManagedObject>({ManagedBacking? backing}) {
if (backing != null) {
return (runtime.instanceOfImplementation(backing: backing)..entity = this)
as T;
}
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>?;
}
/// Returns an attribute in this entity for a property selector.
///
/// Invokes [identifyProperties] with [propertyIdentifier], and ensures that a single attribute
/// on this entity was selected. Returns that attribute.
ManagedAttributeDescription identifyAttribute<T, U extends ManagedObject>(
T Function(U x) propertyIdentifier,
) {
final keyPaths = identifyProperties(propertyIdentifier);
if (keyPaths.length != 1) {
throw ArgumentError(
"Invalid property selector. Cannot access more than one property for this operation.",
);
}
final firstKeyPath = keyPaths.first;
if (firstKeyPath.dynamicElements != null) {
throw ArgumentError(
"Invalid property selector. Cannot access subdocuments for this operation.",
);
}
final elements = firstKeyPath.path;
if (elements.length > 1) {
throw ArgumentError(
"Invalid property selector. Cannot use relationships for this operation.",
);
}
final propertyName = elements.first!.name;
final attribute = attributes[propertyName];
if (attribute == null) {
if (relationships.containsKey(propertyName)) {
throw ArgumentError(
"Invalid property selection. Property '$propertyName' on "
"'$name' "
"is a relationship and cannot be selected for this operation.");
} else {
throw ArgumentError(
"Invalid property selection. Column '$propertyName' does not "
"exist on table '$tableName'.");
}
}
return attribute;
}
/// Returns a relationship in this entity for a property selector.
///
/// Invokes [identifyProperties] with [propertyIdentifier], and ensures that a single relationship
/// on this entity was selected. Returns that relationship.
ManagedRelationshipDescription
identifyRelationship<T, U extends ManagedObject>(
T Function(U x) propertyIdentifier,
) {
final keyPaths = identifyProperties(propertyIdentifier);
if (keyPaths.length != 1) {
throw ArgumentError(
"Invalid property selector. Cannot access more than one property for this operation.",
);
}
final firstKeyPath = keyPaths.first;
if (firstKeyPath.dynamicElements != null) {
throw ArgumentError(
"Invalid property selector. Cannot access subdocuments for this operation.",
);
}
final elements = firstKeyPath.path;
if (elements.length > 1) {
throw ArgumentError(
"Invalid property selector. Cannot identify a nested relationship for this operation.",
);
}
final propertyName = elements.first!.name;
final desc = relationships[propertyName];
if (desc == null) {
throw ArgumentError(
"Invalid property selection. Relationship named '$propertyName' on table '$tableName' is not a relationship.",
);
}
return desc;
}
/// Returns a property selected by [propertyIdentifier].
///
/// Invokes [identifyProperties] with [propertyIdentifier], and ensures that a single property
/// on this entity was selected. Returns that property.
KeyPath identifyProperty<T, U extends ManagedObject>(
T Function(U x) propertyIdentifier,
) {
final properties = identifyProperties(propertyIdentifier);
if (properties.length != 1) {
throw ArgumentError(
"Invalid property selector. Must reference a single property only.",
);
}
return properties.first;
}
/// Returns a list of properties selected by [propertiesIdentifier].
///
/// Each selected property in [propertiesIdentifier] is returned in a [KeyPath] object that fully identifies the
/// property relative to this entity.
List<KeyPath> identifyProperties<T, U extends ManagedObject>(
T Function(U x) propertiesIdentifier,
) {
final tracker = ManagedAccessTrackingBacking();
final obj = instanceOf<U>(backing: tracker);
propertiesIdentifier(obj);
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;
final buffer = StringBuffer();
if (uniquePropertySet != null) {
final propString =
uniquePropertySet!.map((s) => "'${s.name}'").join(", ");
buffer.writeln(
"No two objects may have the same value for all of: $propString.",
);
}
obj.description = buffer.toString();
properties.forEach((name, def) {
if (def is ManagedAttributeDescription &&
!def.isIncludedInDefaultResultSet &&
!def.isTransient) {
return;
}
final schemaProperty = def!.documentSchemaObject(context);
schemaProperties[name] = schemaProperty;
});
return obj;
}
/// 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();
buf.writeln("Entity: $tableName");
buf.writeln("Attributes:");
attributes.forEach((name, attr) {
buf.writeln("\t$attr");
});
buf.writeln("Relationships:");
relationships.forEach((name, rel) {
buf.writeln("\t$rel");
});
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);
context.schema.register(name, obj, representation: instanceType);
}
}
/// 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,19 +0,0 @@
/*
* 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.
///
/// 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,151 +0,0 @@
/*
* 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,45 +0,0 @@
/*
* 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';
export 'document.dart';
export 'entity.dart';
export 'exception.dart';
export 'object.dart';
export 'property_description.dart';
export 'set.dart';
export 'type.dart';
export 'validation/managed.dart';
export 'validation/metadata.dart';
export 'key_path.dart';

View file

@ -1,469 +0,0 @@
/*
* 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/http.dart';
import 'package:protevus_openapi/v3.dart';
import 'package:meta/meta.dart';
/// An abstract class that provides storage for [ManagedObject] instances.
///
/// This class is primarily used internally.
///
/// A [ManagedObject] stores properties declared by its type argument in instances of this type.
/// Values are validated against the [ManagedObject.entity].
///
/// Instances of this type only store properties for which a value has been explicitly set. This allows
/// serialization classes to omit unset values from the serialized values. Therefore, instances of this class
/// provide behavior that can differentiate between a property being the null value and a property simply not being
/// set. (Therefore, you must use [removeProperty] instead of setting a value to null to really remove it from instances
/// of this type.)
///
/// Conduit implements concrete subclasses of this class to provide behavior for property storage
/// and query-building.
abstract class ManagedBacking {
/// 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 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 the backing map of this [ManagedBacking] instance.
///
/// Use this method to use any reference of a property from this instance.
void removeProperty(String propertyName) {
contents.remove(propertyName);
}
/// 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 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.
///
/// A managed object is declared in two parts, the subclass and its table definition.
///
/// class User extends ManagedObject<_User> implements _User {
/// String name;
/// }
/// class _User {
/// @primaryKey
/// int id;
///
/// @Column(indexed: true)
/// String email;
/// }
///
/// Table definitions are plain Dart objects that represent a database table. Each property is a column in the database.
///
/// A subclass of this type must implement its table definition and use it as the type argument of [ManagedObject]. Properties and methods
/// declared in the subclass (also called the 'instance type') are not stored in the database.
///
/// 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;
/// 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.
///
/// 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) {
throw ArgumentError("Invalid property access for '${entity.name}'. "
"Property '$propertyName' does not exist on '${entity.name}'.");
}
return backing.valueForProperty(prop);
}
/// 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) {
throw ArgumentError("Invalid property access for '${entity.name}'. "
"Property '$propertyName' does not exist on '${entity.name}'.");
}
backing.setValueForProperty(prop, value);
}
/// Removes a property from [backing].
///
/// 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);
}
}
/// 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);
}
/// Callback to modify an object prior to updating it with a [Query].
///
/// Subclasses of this type may override this method to set or modify values prior to being updated
/// via [Query.update] or [Query.updateOne]. It is automatically invoked by [Query.update] and [Query.updateOne].
///
/// 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 'updatedDate' of an object each time it was updated:
///
/// @override
/// void willUpdate() {
/// updatedDate = new DateTime.now().toUtc();
/// }
///
/// This method is only invoked when a query is configured by its [Query.values]. This method is not invoked
/// if [Query.valueMap] is used to configure a query.
void willUpdate() {}
/// Callback to modify an object prior to inserting it with a [Query].
///
/// Subclasses of this type may override this method to set or modify values prior to being inserted
/// via [Query.insert]. It is automatically invoked by [Query.insert].
///
/// 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:
/// @override
/// void willInsert() {
/// createdDate = new DateTime.now().toUtc();
/// }
///
/// This method is only invoked when a query is configured by its [Query.values]. This method is not invoked
/// if [Query.valueMap] is used to configure a query.
void willInsert() {}
/// Validates an object according to its property [Validate] metadata.
///
/// This method is invoked by [Query] when inserting or updating an instance of this type. By default,
/// this method runs all of the [Validate] metadata for each property of this instance's persistent type. See [Validate]
/// for more information. If validations succeed, the returned context [ValidationContext.isValid] will be true. Otherwise,
/// it is false and all errors are available in [ValidationContext.errors].
///
/// This method returns the result of [ManagedValidator.run]. You may override this method to provide additional validation
/// prior to insertion or deletion. If you override this method, you *must* invoke the super implementation to
/// allow [Validate] annotations to run, e.g.:
///
/// ValidationContext validate({Validating forEvent: Validating.insert}) {
/// var context = super(forEvent: forEvent);
///
/// if (a + b > 10) {
/// context.addError("a + b > 10");
/// }
///
/// return context;
/// }
@mustCallSuper
ValidationContext validate({Validating forEvent = Validating.insert}) {
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);
if (propertyName != null) {
if (invocation.isGetter) {
return this[propertyName];
} else if (invocation.isSetter) {
this[propertyName] = invocation.positionalArguments.first;
return null;
}
}
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) {
final property = responseKeyProperties[key];
if (property == null) {
throw ValidationException(["invalid input key '$key'"]);
}
if (property.isPrivate) {
throw ValidationException(["invalid input key '$key'"]);
}
if (property is ManagedAttributeDescription) {
if (!property.isTransient) {
backing.setValueForProperty(
property,
property.convertFromPrimitiveValue(v),
);
} else {
if (!property.transientStatus!.isAvailableAsInput) {
throw ValidationException(["invalid input key '$key'"]);
}
final decodedValue = property.convertFromPrimitiveValue(v);
if (!property.isAssignableWith(decodedValue)) {
throw ValidationException(["invalid input type for key '$key'"]);
}
entity.runtime
.setTransientValueForKey(this, property.name, decodedValue);
}
} else {
backing.setValueForProperty(
property,
property.convertFromPrimitiveValue(v),
);
}
});
}
/// Converts this instance into a serializable map.
///
/// This method returns a map of the key-values pairs in this instance. This value is typically converted into a transmission format like JSON.
///
/// Only properties present in [backing] are serialized, otherwise, they are omitted from the map. If a property is present in [backing] and the value is null,
/// the value null will be serialized for that property key.
///
/// Usage:
/// var json = json.encode(model.asMap());
@override
Map<String, dynamic> asMap() {
final outputMap = <String, dynamic>{};
backing.contents.forEach((k, v) {
if (!_isPropertyPrivate(k)) {
final property = properties[k];
final value = property!.convertToPrimitiveValue(v);
if (value == null && !_includeIfNull(property)) {
return;
}
outputMap[mapKeyName(k)] = value;
}
});
entity.attributes.values
.where((attr) => attr!.transientStatus?.isAvailableAsOutput ?? false)
.forEach((attr) {
final value = entity.runtime.getTransientValueForKey(this, attr!.name);
if (value != null) {
outputMap[mapKeyName(attr.responseKey?.name ?? attr.name)] = value;
}
});
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,973 +0,0 @@
/*
* 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';
import 'package:protevus_database/src/persistent_store/persistent_store.dart';
import 'package:protevus_database/src/query/query.dart';
import 'package:protevus_openapi/v3.dart';
import 'package:protevus_runtime/runtime.dart';
/// Contains database column information and metadata for a property of a [ManagedObject] object.
///
/// Each property a [ManagedObject] object manages is described by an instance of [ManagedPropertyDescription], which contains useful information
/// 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,
this.type,
this.declaredType, {
bool unique = false,
bool indexed = false,
bool nullable = false,
bool includedInDefaultResultSet = true,
this.autoincrement = false,
List<ManagedValidator> validators = const [],
this.responseModel,
this.responseKey,
}) : isUnique = unique,
isIndexed = indexed,
isNullable = nullable,
isIncludedInDefaultResultSet = includedInDefaultResultSet,
_validators = validators {
for (final v in _validators) {
v.property = this;
}
}
/// 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.
///
/// 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].
///
/// 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].
///
/// 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.
///
/// 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.
final bool isIncludedInDefaultResultSet;
/// Whether or not this property should use an auto-incrementing scheme.
///
/// 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;
/// 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.
///
/// This flag is not included in schemas documents used by database migrations and other tools.
bool get isPrivate {
return name.startsWith("_");
}
/// 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;
/// 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.
///
/// This method takes a Dart representation of a value and converts it to something that can
/// be used elsewhere (e.g. an HTTP body or database query). How this value is computed
/// depends on this instance's definition.
///
/// Parameters:
/// - [value]: The Dart representation of the value to be converted.
///
/// Returns:
/// The converted primitive value.
dynamic convertToPrimitiveValue(dynamic value);
/// Converts a value to a more complex value from a primitive value according to this instance's definition.
///
/// This method takes a non-Dart representation of a value (e.g. an HTTP body or database query)
/// and turns it into a Dart representation. How this value is computed
/// depends on this instance's definition.
dynamic convertFromPrimitiveValue(dynamic value);
/// The type of the variable that this property represents.
///
/// This property represents the Dart type of the variable that the [ManagedPropertyDescription] instance
/// is describing. It is used to ensure that the value assigned to this property is compatible with the
/// expected type.
final Type? declaredType;
/// Returns an [APISchemaObject] that represents this property.
///
/// 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:
return APISchemaObject.integer();
case ManagedPropertyType.bigInteger:
return APISchemaObject.integer();
case ManagedPropertyType.doublePrecision:
return APISchemaObject.number();
case ManagedPropertyType.string:
return APISchemaObject.string();
case ManagedPropertyType.datetime:
return APISchemaObject.string(format: "date-time");
case ManagedPropertyType.boolean:
return APISchemaObject.boolean();
case ManagedPropertyType.list:
return APISchemaObject.array(
ofSchema: _typedSchemaObject(type.elements!),
);
case ManagedPropertyType.map:
return APISchemaObject.map(
ofSchema: _typedSchemaObject(type.elements!),
);
case ManagedPropertyType.document:
return APISchemaObject.freeForm();
}
// throw UnsupportedError("Unsupported type '$type' when documenting entity.");
}
}
/// Stores the specifics of database columns in [ManagedObject]s as indicated by [Column].
///
/// This class is used internally to manage data models. For specifying these attributes,
/// see [Column].
///
/// Attributes are the scalar values of a [ManagedObject] (as opposed to relationship values,
/// which are [ManagedRelationshipDescription] instances).
///
/// 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,
ManagedType super.type,
super.declaredType, {
this.transientStatus,
bool primaryKey = false,
this.defaultValue,
super.unique,
super.indexed,
super.nullable,
super.includedInDefaultResultSet,
super.autoincrement,
super.validators,
super.responseModel,
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,
ManagedType super.type,
Type super.declaredType,
this.transientStatus, {
super.responseKey,
}) : isPrimaryKey = false,
defaultValue = null,
super(
unique: false,
indexed: false,
nullable: false,
includedInDefaultResultSet: false,
autoincrement: false,
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,
ManagedType type, {
Serialize? transientStatus,
bool primaryKey = false,
String? defaultValue,
bool unique = false,
bool indexed = false,
bool nullable = false,
bool includedInDefaultResultSet = true,
bool autoincrement = false,
List<ManagedValidator> validators = const [],
ResponseKey? responseKey,
ResponseModel? responseModel,
}) {
return ManagedAttributeDescription(
entity,
name,
type,
T,
transientStatus: transientStatus,
primaryKey: primaryKey,
defaultValue: defaultValue,
unique: unique,
indexed: indexed,
nullable: nullable,
includedInDefaultResultSet: includedInDefaultResultSet,
autoincrement: autoincrement,
validators: validators,
responseKey: responseKey,
responseModel: responseModel,
);
}
/// 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, 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;
/// 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.
///
/// 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:
///
/// {
/// "option1": Options.option1,
/// "option2": Options.option2
/// }
///
Map<String, dynamic> get enumerationValueMap => type!.enumerationMap;
/// 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;
/// 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!)
..title = name;
final buf = StringBuffer();
// Add'l schema info
prop.isNullable = isNullable;
for (final v in validators) {
v.definition.constrainSchemaObject(context, prop);
}
if (isEnumeratedValue) {
prop.enumerated = prop.enumerated!.map(convertToPrimitiveValue).toList();
}
if (isTransient) {
if (transientStatus!.isAvailableAsInput &&
!transientStatus!.isAvailableAsOutput) {
prop.isWriteOnly = true;
} else if (!transientStatus!.isAvailableAsInput &&
transientStatus!.isAvailableAsOutput) {
prop.isReadOnly = true;
}
}
if (isUnique) {
buf.writeln("No two objects may have the same value for this field.");
}
if (isPrimaryKey) {
buf.writeln("This is the primary identifier for this object.");
}
if (defaultValue != null) {
prop.defaultValue = defaultValue;
}
if (buf.isNotEmpty) {
prop.description = buf.toString();
}
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();
if (isPrimaryKey) {
flagBuffer.write("primary_key ");
}
if (isTransient) {
flagBuffer.write("transient ");
}
if (autoincrement) {
flagBuffer.write("autoincrementing ");
}
if (isUnique) {
flagBuffer.write("unique ");
}
if (defaultValue != null) {
flagBuffer.write("defaults to $defaultValue ");
}
if (isIndexed) {
flagBuffer.write("indexed ");
}
if (isNullable) {
flagBuffer.write("nullable ");
} else {
flagBuffer.write("required ");
}
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) {
return null;
}
if (type!.kind == ManagedPropertyType.datetime && value is DateTime) {
return value.toIso8601String();
} else if (isEnumeratedValue) {
// todo: optimize?
return value.toString().split(".").last;
} else if (type!.kind == ManagedPropertyType.document &&
value is Document) {
return value.data;
}
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) {
return null;
}
if (type!.kind == ManagedPropertyType.datetime) {
if (value is! String) {
throw ValidationException(["invalid input value for '$name'"]);
}
return DateTime.parse(value);
} else if (type!.kind == ManagedPropertyType.doublePrecision) {
if (value is! num) {
throw ValidationException(["invalid input value for '$name'"]);
}
return value.toDouble();
} else if (isEnumeratedValue) {
if (!enumerationValueMap.containsKey(value)) {
throw ValidationException(["invalid option for key '$name'"]);
}
return enumerationValueMap[value];
} else if (type!.kind == ManagedPropertyType.document) {
return Document(value);
} else if (type!.kind == ManagedPropertyType.list ||
type!.kind == ManagedPropertyType.map) {
try {
return entity.runtime.dynamicConvertFromPrimitiveValue(this, value);
} on TypeCoercionException {
throw ValidationException(["invalid input value for '$name'"]);
}
}
return value;
}
}
/// 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,
super.type,
super.declaredType,
this.destinationEntity,
this.deleteRule,
this.relationshipType,
this.inverseKey, {
super.unique,
super.indexed,
super.nullable,
super.includedInDefaultResultSet,
super.validators = const [],
super.responseModel,
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,
ManagedType? type,
ManagedEntity destinationEntity,
DeleteRule? deleteRule,
ManagedRelationshipType relationshipType,
String inverseKey, {
bool unique = false,
bool indexed = false,
bool nullable = false,
bool includedInDefaultResultSet = true,
List<ManagedValidator> validators = const [],
ResponseKey? responseKey,
ResponseModel? responseModel,
}) {
return ManagedRelationshipDescription(
entity,
name,
type,
T,
destinationEntity,
deleteRule,
relationshipType,
inverseKey,
unique: unique,
indexed: indexed,
nullable: nullable,
includedInDefaultResultSet: includedInDefaultResultSet,
validators: validators,
responseKey: responseKey,
responseModel: responseModel,
);
}
/// 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 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 [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;
/// 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];
/// 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;
/// 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) {
return destinationEntity.runtime.isValueListOf(dartValue);
}
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) {
return value
.map((ManagedObject innerValue) => innerValue.asMap())
.toList();
} else if (value is ManagedObject) {
// If we're only fetching the foreign key, don't do a full asMap
if (relationshipType == ManagedRelationshipType.belongsTo &&
value.backing.contents.length == 1 &&
value.backing.contents.containsKey(destinationEntity.primaryKey)) {
return <String, Object>{
destinationEntity.primaryKey: value[destinationEntity.primaryKey]
};
}
return value.asMap();
} else if (value == null) {
return null;
}
throw StateError(
"Invalid relationship assigment. Relationship '$entity.$name' is not a 'ManagedSet' or 'ManagedObject'.",
);
}
/// 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) {
return null;
}
if (relationshipType == ManagedRelationshipType.belongsTo ||
relationshipType == ManagedRelationshipType.hasOne) {
if (value is! Map<String, dynamic>) {
throw ValidationException(["invalid input type for '$name'"]);
}
final instance = destinationEntity.instanceOf()..readFromMap(value);
return instance;
}
/* else if (relationshipType == ManagedRelationshipType.hasMany) { */
if (value is! List) {
throw ValidationException(["invalid input type for '$name'"]);
}
ManagedObject instantiator(dynamic m) {
if (m is! Map<String, dynamic>) {
throw ValidationException(["invalid input type for '$name'"]);
}
final instance = destinationEntity.instanceOf()..readFromMap(m);
return instance;
}
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 =
context.schema.getObjectWithType(inverse!.entity.instanceType);
if (relationshipType == ManagedRelationshipType.hasMany) {
return APISchemaObject.array(ofSchema: relatedType)
..isReadOnly = true
..isNullable = true;
} else if (relationshipType == ManagedRelationshipType.hasOne) {
return relatedType
..isReadOnly = true
..isNullable = true;
}
final destPk = destinationEntity.primaryKeyAttribute!;
return APISchemaObject.object({
destPk.name: ManagedPropertyDescription._typedSchemaObject(destPk.type!)
})
..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";
switch (relationshipType) {
case ManagedRelationshipType.belongsTo:
relTypeString = "belongs to";
break;
case ManagedRelationshipType.hasMany:
relTypeString = "has-many";
break;
case ManagedRelationshipType.hasOne:
relTypeString = "has-a";
break;
// case null:
// relTypeString = 'Not set';
// break;
}
return "- $name -> '${destinationEntity.name}' | Type: $relTypeString | Inverse: $inverseKey";
}
}

View file

@ -1,18 +0,0 @@
/*
* 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,109 +0,0 @@
/*
* 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.
///
/// 'Has many' relationship properties in [ManagedObject]s are represented by this type. [ManagedSet]s properties may only be declared in the persistent
/// type of a [ManagedObject]. Example usage:
///
/// class User extends ManagedObject<_User> implements _User {}
/// class _User {
/// ...
/// ManagedSet<Post> posts;
/// }
///
/// class Post extends ManagedObject<_Post> implements _Post {}
/// class _Post {
/// ...
/// @Relate(#posts)
/// User user;
/// }
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 [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] 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 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];
/// 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,236 +0,0 @@
/*
* 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,
/// Represented by instances of [int].
bigInteger,
/// Represented by instances of [String].
string,
/// Represented by instances of [DateTime].
datetime,
/// Represented by instances of [bool].
boolean,
/// Represented by instances of [double].
doublePrecision,
/// Represented by instances of [Map].
map,
/// Represented by instances of [List].
list,
/// Represented by instances of [Document]
document
}
/// 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 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,
Map<String, dynamic> enumerationMap,
) {
return ManagedType(T, kind, elements, enumerationMap);
}
/// The primitive kind of this type.
///
/// 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 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;
/// The Dart type represented by this [ManagedType] instance.
final Type 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;
/// 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;
}
switch (kind) {
case ManagedPropertyType.bigInteger:
return dartValue is int;
case ManagedPropertyType.integer:
return dartValue is int;
case ManagedPropertyType.boolean:
return dartValue is bool;
case ManagedPropertyType.datetime:
return dartValue is DateTime;
case ManagedPropertyType.doublePrecision:
return dartValue is double;
case ManagedPropertyType.map:
return dartValue is Map<String, dynamic>;
case ManagedPropertyType.list:
return dartValue is List<dynamic>;
case ManagedPropertyType.document:
return dartValue is Document;
case ManagedPropertyType.string:
{
if (enumerationMap.isNotEmpty) {
return enumerationMap.values.contains(dartValue);
}
return dartValue is String;
}
}
}
/// 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,129 +0,0 @@
/*
* 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,
lessThanEqualTo,
greaterThan,
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:
{
if (comparisonValue!.compareTo(input) != 0) {
context.addError("must be equal to '$comparisonValue'.");
}
}
break;
case ValidationOperator.greaterThan:
{
if (comparisonValue!.compareTo(input) >= 0) {
context.addError("must be greater than '$comparisonValue'.");
}
}
break;
case ValidationOperator.greaterThanEqualTo:
{
if (comparisonValue!.compareTo(input) > 0) {
context.addError(
"must be greater than or equal to '$comparisonValue'.",
);
}
}
break;
case ValidationOperator.lessThan:
{
if (comparisonValue!.compareTo(input) <= 0) {
context.addError("must be less than to '$comparisonValue'.");
}
}
break;
case ValidationOperator.lessThanEqualTo:
{
if (comparisonValue!.compareTo(input) < 0) {
context
.addError("must be less than or equal to '$comparisonValue'.");
}
}
break;
}
}
}

View file

@ -1,153 +0,0 @@
/*
* 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';
/// Validates properties of [ManagedObject] before an insert or update [Query].
///
/// 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].
///
/// Validates the properties of [object] according to its validator annotations. Validators
/// are added to properties using [Validate] metadata.
///
/// This method does not invoke [ManagedObject.validate] - any customization provided
/// by a [ManagedObject] subclass that overrides this method will not be invoked.
static ValidationContext run(
ManagedObject object, {
Validating event = Validating.insert,
}) {
final context = ValidationContext();
for (final validator in object.entity.validators) {
context.property = validator.property;
context.event = event;
context.state = validator.state;
if (!validator.definition.runOnInsert && event == Validating.insert) {
continue;
}
if (!validator.definition.runOnUpdate && event == Validating.update) {
continue;
}
var contents = object.backing.contents;
String key = validator.property!.name;
if (validator.definition.type == ValidateType.present) {
if (validator.property is ManagedRelationshipDescription) {
final inner = object[validator.property!.name] as ManagedObject?;
if (inner == null ||
!inner.backing.contents.containsKey(inner.entity.primaryKey)) {
context.addError("key '${validator.property!.name}' is required "
"for ${_getEventName(event)}s.");
}
} else if (!contents.containsKey(key)) {
context.addError("key '${validator.property!.name}' is required "
"for ${_getEventName(event)}s.");
}
} else if (validator.definition.type == ValidateType.absent) {
if (validator.property is ManagedRelationshipDescription) {
final inner = object[validator.property!.name] as ManagedObject?;
if (inner != null) {
context.addError("key '${validator.property!.name}' is not allowed "
"for ${_getEventName(event)}s.");
}
} else if (contents.containsKey(key)) {
context.addError("key '${validator.property!.name}' is not allowed "
"for ${_getEventName(event)}s.");
}
} else {
if (validator.property is ManagedRelationshipDescription) {
final inner = object[validator.property!.name] as ManagedObject?;
if (inner == null ||
inner.backing.contents[inner.entity.primaryKey] == null) {
continue;
}
contents = inner.backing.contents;
key = inner.entity.primaryKey;
}
final value = contents[key];
if (value != null) {
validator.validate(context, value);
}
}
}
return context;
}
/// 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:
return "insert";
case Validating.update:
return "update";
default:
return "unknown";
}
}
}

View file

@ -1,917 +0,0 @@
/*
* 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.
class ValidationContext {
/// Whether this validation is occurring during update or insert.
late Validating event;
/// The property being validated.
ManagedPropertyDescription? property;
/// State associated with the validator being run.
///
/// Use this property in a custom validator to access compiled state. Compiled state
/// is a value that has been computed from the arguments to the validator. For example,
/// a 'greater than 1' validator, the state is an expression object that evaluates
/// a value is greater than 1.
///
/// Set this property by returning the desired value from [Validate.compare].
dynamic state;
/// Errors that have occurred in this context.
List<String> errors = [];
/// Adds a validation error to the context.
///
/// A validation will fail if this method is invoked.
void addError(String reason) {
final p = property;
if (p is ManagedRelationshipDescription) {
errors.add(
"${p.entity.name}.${p.name}.${p.destinationEntity.primaryKey}: $reason",
);
} else {
errors.add("${p!.entity.name}.${p.name}: $reason");
}
}
/// Whether this validation context passed all validations.
bool get isValid => errors.isEmpty;
}
/// An error thrown during validator compilation.
///
/// If you override [Validate.compile], throw errors of this type if a validator
/// is applied to an invalid property.
class ValidateCompilationError extends Error {
ValidateCompilationError(this.reason);
/// The reason for the [ValidateCompilationError].
final String reason;
}
/// Add as metadata to persistent properties to validate their values before insertion or updating.
///
/// When executing update or insert queries, any properties with this metadata will be validated
/// against the condition declared by this instance. Example:
///
/// class Person extends ManagedObject<_Person> implements _Person {}
/// class _Person {
/// @primaryKey
/// int id;
///
/// @Validate.length(greaterThan: 10)
/// String name;
/// }
///
/// Properties may have more than one metadata of this type. All validations must pass
/// for an insert or update to be valid.
///
/// By default, validations occur on update and insert queries. Constructors have arguments
/// for only running a validation on insert or update. See [runOnUpdate] and [runOnInsert].
///
/// This class may be subclassed to create custom validations. Subclasses must override [validate].
class Validate {
/// Invoke this constructor when creating custom subclasses.
///
/// 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})
/// : super(onUpdate: onUpdate, onInsert: onInsert);
///
/// bool validate(
/// ValidateOperation operation,
/// ManagedAttributeDescription property,
/// String value,
/// List<String> errors) {
/// return someCondition;
/// }
/// }
const Validate({bool onUpdate = true, bool onInsert = true})
: runOnUpdate = onUpdate,
runOnInsert = onInsert,
_value = null,
_lessThan = null,
_lessThanEqualTo = null,
_greaterThan = null,
_greaterThanEqualTo = null,
_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,
ValidateType? validator,
dynamic value,
Comparable? greaterThan,
Comparable? greaterThanEqualTo,
Comparable? equalTo,
Comparable? lessThan,
Comparable? lessThanEqualTo,
}) : runOnUpdate = onUpdate,
runOnInsert = onInsert,
type = validator,
_value = value,
_greaterThan = greaterThan,
_greaterThanEqualTo = greaterThanEqualTo,
_equalTo = equalTo,
_lessThan = lessThan,
_lessThanEqualTo = lessThanEqualTo;
/// A validator for matching an input String against a regular expression.
///
/// Values passing through validators of this type must match a regular expression
/// created by [pattern]. See [RegExp] in the Dart standard library for behavior.
///
/// This validator is only valid for [String] properties.
///
/// If [onUpdate] is true (the default), this validation is run on update queries.
/// If [onInsert] is true (the default), this validation is run on insert queries.
const Validate.matches(
String pattern, {
bool onUpdate = true,
bool onInsert = true,
}) : this._(
value: pattern,
onUpdate: onUpdate,
onInsert: onInsert,
validator: ValidateType.regex,
);
/// A validator for comparing a value.
///
/// Values passing through validators of this type must be [lessThan],
/// [greaterThan], [lessThanEqualTo], [equalTo], or [greaterThanEqualTo
/// to the value provided for each argument.
///
/// Any argument not specified is not evaluated. A typical validator
/// only uses one argument:
///
/// @Validate.compare(lessThan: 10.0)
/// double value;
///
/// All provided arguments are evaluated. Therefore, the following
/// requires an input value to be between 6 and 10:
///
/// @Validate.compare(greaterThanEqualTo: 6, lessThanEqualTo: 10)
/// int value;
///
/// This validator can be used for [String], [double], [int] and [DateTime] properties.
///
/// When creating a validator for [DateTime] properties, the value for an argument
/// is a [String] that will be parsed by [DateTime.parse].
///
/// @Validate.compare(greaterThan: "2017-02-11T00:30:00Z")
/// DateTime date;
///
/// If [onUpdate] is true (the default), this validation is run on update queries.
/// If [onInsert] is true (the default), this validation is run on insert queries.
const Validate.compare({
Comparable? lessThan,
Comparable? greaterThan,
Comparable? equalTo,
Comparable? greaterThanEqualTo,
Comparable? lessThanEqualTo,
bool onUpdate = true,
bool onInsert = true,
}) : this._(
lessThan: lessThan,
lessThanEqualTo: lessThanEqualTo,
greaterThan: greaterThan,
greaterThanEqualTo: greaterThanEqualTo,
equalTo: equalTo,
onUpdate: onUpdate,
onInsert: onInsert,
validator: ValidateType.comparison,
);
/// A validator for validating the length of a [String].
///
/// Values passing through validators of this type must a [String] with a length that is[lessThan],
/// [greaterThan], [lessThanEqualTo], [equalTo], or [greaterThanEqualTo
/// to the value provided for each argument.
///
/// Any argument not specified is not evaluated. A typical validator
/// only uses one argument:
///
/// @Validate.length(lessThan: 10)
/// String foo;
///
/// All provided arguments are evaluated. Therefore, the following
/// requires an input string to have a length to be between 6 and 10:
///
/// @Validate.length(greaterThanEqualTo: 6, lessThanEqualTo: 10)
/// String foo;
///
/// If [onUpdate] is true (the default), this validation is run on update queries.
/// If [onInsert] is true (the default), this validation is run on insert queries.
const Validate.length({
int? lessThan,
int? greaterThan,
int? equalTo,
int? greaterThanEqualTo,
int? lessThanEqualTo,
bool onUpdate = true,
bool onInsert = true,
}) : this._(
lessThan: lessThan,
lessThanEqualTo: lessThanEqualTo,
greaterThan: greaterThan,
greaterThanEqualTo: greaterThanEqualTo,
equalTo: equalTo,
onUpdate: onUpdate,
onInsert: onInsert,
validator: ValidateType.length,
);
/// A validator for ensuring a property always has a value when being inserted or updated.
///
/// This metadata requires that a property must be set in [Query.values] before an update
/// or insert. The value may be null, if the property's [Column.isNullable] allow it.
///
/// If [onUpdate] is true (the default), this validation requires a property to be present for update queries.
/// If [onInsert] is true (the default), this validation requires a property to be present for insert queries.
const Validate.present({bool onUpdate = true, bool onInsert = true})
: this._(
onUpdate: onUpdate,
onInsert: onInsert,
validator: ValidateType.present,
);
/// A validator for ensuring a property does not have a value when being inserted or updated.
///
/// This metadata requires that a property must NOT be set in [Query.values] before an update
/// or insert.
///
/// This validation is used to restrict input during either an insert or update query. For example,
/// a 'dateCreated' property would use this validator to ensure that property isn't set during an update.
///
/// @Validate.absent(onUpdate: true, onInsert: false)
/// DateTime dateCreated;
///
/// If [onUpdate] is true (the default), this validation requires a property to be absent for update queries.
/// If [onInsert] is true (the default), this validation requires a property to be absent for insert queries.
const Validate.absent({bool onUpdate = true, bool onInsert = true})
: this._(
onUpdate: onUpdate,
onInsert: onInsert,
validator: ValidateType.absent,
);
/// A validator for ensuring a value is one of a set of values.
///
/// An input value must be one of [values].
///
/// [values] must be homogenous - every value must be the same type -
/// and the property with this metadata must also match the type
/// of the objects in [values].
///
/// This validator can be used for [String] and [int] properties.
///
/// @Validate.oneOf(const ["A", "B", "C")
/// String foo;
///
/// If [onUpdate] is true (the default), this validation is run on update queries.
/// If [onInsert] is true (the default), this validation is run on insert queries.
const Validate.oneOf(
List<dynamic> values, {
bool onUpdate = true,
bool onInsert = true,
}) : this._(
value: values,
onUpdate: onUpdate,
onInsert: onInsert,
validator: ValidateType.oneOf,
);
/// A validator that ensures a value cannot be modified after insertion.
///
/// 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.
///
/// Use this method to ensure a validator is being applied to a property correctly. For example, a
/// [Validate.compare] builds a list of expressions and ensures each expression's values are the
/// same type as the property being validated.
///
/// The value returned from this method is available in [ValidationContext.state] when this
/// instance's [validate] method is called.
///
/// [typeBeingValidated] is the type of the property being validated. If [relationshipInverseType] is not-null,
/// it is a [ManagedObject] subclass and [typeBeingValidated] is the type of its primary key.
///
/// If compilation fails, throw a [ValidateCompilationError] with a message describing the issue. The entity
/// and property will automatically be added to the error.
dynamic compile(
ManagedType typeBeingValidated, {
Type? relationshipInverseType,
}) {
switch (type) {
case ValidateType.absent:
return null;
case ValidateType.present:
return null;
case ValidateType.oneOf:
{
return _oneOfCompiler(
typeBeingValidated,
relationshipInverseType: relationshipInverseType,
);
}
case ValidateType.comparison:
return _comparisonCompiler(
typeBeingValidated,
relationshipInverseType: relationshipInverseType,
);
case ValidateType.regex:
return _regexCompiler(
typeBeingValidated,
relationshipInverseType: relationshipInverseType,
);
case ValidateType.length:
return _lengthCompiler(
typeBeingValidated,
relationshipInverseType: relationshipInverseType,
);
default:
return null;
}
}
/// Validates the [input] value.
///
/// Subclasses override this method to provide validation behavior.
///
/// [input] is the value being validated. If the value is invalid, the reason
/// is added to [context] via [ValidationContext.addError].
///
/// Additional information about the validation event and the attribute being evaluated
/// is available in [context].
/// in [context].
///
/// This method is not run when [input] is null.
///
/// The type of [input] will have already been type-checked prior to executing this method.
void validate(ValidationContext context, dynamic input) {
switch (type!) {
case ValidateType.absent:
{}
break;
case ValidateType.present:
{}
break;
case ValidateType.comparison:
{
final expressions = context.state as List<ValidationExpression>;
for (final expr in expressions) {
expr.compare(context, input);
}
}
break;
case ValidateType.regex:
{
final regex = context.state as RegExp;
if (!regex.hasMatch(input as String)) {
context.addError("does not match pattern ${regex.pattern}");
}
}
break;
case ValidateType.oneOf:
{
final options = context.state as List<dynamic>;
if (options.every((v) => input != v)) {
context.addError(
"must be one of: ${options.map((v) => "'$v'").join(",")}.",
);
}
}
break;
case ValidateType.length:
{
final expressions = context.state as List<ValidationExpression>;
for (final expr in expressions) {
expr.compare(context, (input as String).length);
}
}
break;
}
}
/// Adds constraints to an [APISchemaObject] imposed by this validator.
///
/// 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:
{
object.pattern = _value as String?;
}
break;
case ValidateType.comparison:
{
if (_greaterThan is num) {
object.exclusiveMinimum = true;
object.minimum = _greaterThan as num?;
} else if (_greaterThanEqualTo is num) {
object.exclusiveMinimum = false;
object.minimum = _greaterThanEqualTo as num?;
}
if (_lessThan is num) {
object.exclusiveMaximum = true;
object.maximum = _lessThan as num?;
} else if (_lessThanEqualTo is num) {
object.exclusiveMaximum = false;
object.maximum = _lessThanEqualTo as num?;
}
}
break;
case ValidateType.length:
{
if (_equalTo != null) {
object.maxLength = _equalTo as int;
object.minLength = _equalTo;
} else {
if (_greaterThan is int) {
object.minLength = 1 + (_greaterThan);
} else if (_greaterThanEqualTo is int) {
object.minLength = _greaterThanEqualTo as int?;
}
if (_lessThan is int) {
object.maxLength = (-1) + (_lessThan);
} else if (_lessThanEqualTo != null) {
object.maximum = _lessThanEqualTo as int?;
}
}
}
break;
case ValidateType.present:
{}
break;
case ValidateType.absent:
{}
break;
case ValidateType.oneOf:
{
object.enumerated = _value as List<dynamic>?;
}
break;
}
}
/// 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,
}) {
if (_value is! List) {
throw ValidateCompilationError(
"Validate.oneOf value must be a List<T>, where T is the type of the property being validated.",
);
}
final options = _value;
final supportedOneOfTypes = [
ManagedPropertyType.string,
ManagedPropertyType.integer,
ManagedPropertyType.bigInteger
];
if (!supportedOneOfTypes.contains(typeBeingValidated.kind) ||
relationshipInverseType != null) {
throw ValidateCompilationError(
"Validate.oneOf is only valid for String or int types.",
);
}
if (options.any((v) => !typeBeingValidated.isAssignableWith(v))) {
throw ValidateCompilationError(
"Validate.oneOf value must be a List<T>, where T is the type of the property being validated.",
);
}
if (options.isEmpty) {
throw ValidateCompilationError(
"Validate.oneOf must have at least one element.",
);
}
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) {
comparisons
.add(ValidationExpression(ValidationOperator.equalTo, _equalTo));
}
if (_lessThan != null) {
comparisons
.add(ValidationExpression(ValidationOperator.lessThan, _lessThan));
}
if (_lessThanEqualTo != null) {
comparisons.add(
ValidationExpression(
ValidationOperator.lessThanEqualTo,
_lessThanEqualTo,
),
);
}
if (_greaterThan != null) {
comparisons.add(
ValidationExpression(ValidationOperator.greaterThan, _greaterThan),
);
}
if (_greaterThanEqualTo != null) {
comparisons.add(
ValidationExpression(
ValidationOperator.greaterThanEqualTo,
_greaterThanEqualTo,
),
);
}
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,
}) {
final exprs = _expressions;
for (final expr in exprs) {
expr.value = _parseComparisonValue(
expr.value,
typeBeingValidated,
relationshipInverseType: relationshipInverseType,
);
}
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, {
Type? relationshipInverseType,
}) {
if (typeBeingValidated?.kind == ManagedPropertyType.datetime) {
if (referenceValue is String) {
try {
return DateTime.parse(referenceValue);
} on FormatException {
throw ValidateCompilationError(
"Validate.compare value '$referenceValue' cannot be parsed as expected DateTime type.",
);
}
}
throw ValidateCompilationError(
"Validate.compare value '$referenceValue' is not expected DateTime type.",
);
}
if (relationshipInverseType == null) {
if (!typeBeingValidated!.isAssignableWith(referenceValue)) {
throw ValidateCompilationError(
"Validate.compare value '$referenceValue' is not assignable to type of attribute being validated.",
);
}
} else {
if (!typeBeingValidated!.isAssignableWith(referenceValue)) {
throw ValidateCompilationError(
"Validate.compare value '$referenceValue' is not assignable to primary key type of relationship being validated.",
);
}
}
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,
}) {
if (typeBeingValidated?.kind != ManagedPropertyType.string) {
throw ValidateCompilationError(
"Validate.matches is only valid for 'String' properties.",
);
}
if (_value is! String) {
throw ValidateCompilationError(
"Validate.matches argument must be 'String'.",
);
}
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,
}) {
if (typeBeingValidated.kind != ManagedPropertyType.string) {
throw ValidateCompilationError(
"Validate.length is only valid for 'String' properties.",
);
}
final expressions = _expressions;
if (expressions.any((v) => v.value is! int)) {
throw ValidateCompilationError(
"Validate.length arguments must be 'int's.",
);
}
return expressions;
}
}

View file

@ -1,433 +0,0 @@
/*
* 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 }
/// 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].
///
/// 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 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,
int timeoutInSeconds, {
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, {
bool temporary = false,
});
}

View file

@ -1,162 +0,0 @@
/*
* 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';
/// An exception describing an issue with a query.
///
/// 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,
this.underlyingException,
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,
) {
final body = {
"error": message ?? "query failed",
};
if (offendingItems != null && offendingItems.isNotEmpty) {
body["detail"] = "Offending Items: ${offendingItems.join(", ")}";
}
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:
return 400;
case QueryExceptionEvent.transport:
return 503;
case QueryExceptionEvent.conflict:
return 409;
}
}
/// 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.
///
/// [Controller]s interpret this exception to return a status code 409 by default.
conflict,
/// This event is used when the underlying [PersistentStore] cannot reach its database.
///
/// [Controller]s interpret this exception to return a status code 503 by default.
transport,
/// This event is used when the underlying [PersistentStore] reports an issue with the data used in a [Query].
///
/// [Controller]s interpret this exception to return a status code 400 by default.
input,
}

View file

@ -1,492 +0,0 @@
/*
* 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);
final QueryExpression<T, InstanceType> lhs;
}
/// Contains methods for adding logical expressions to properties when building a [Query].
///
/// You do not create instances of this type directly, but instead are returned an instance when selecting a property
/// of an object in [Query.where]. You invoke methods from this type to add an expression to the query for the selected property.
/// Example:
///
/// final query = new Query<Employee>()
/// ..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;
/// 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;
_invertNext = false;
} else {
_expression = expr;
}
}
/// 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':
///
/// final query = new Query<Employee>()
/// ..where((e) => e.name).not.equalTo("Bob");
QueryExpression<T, InstanceType> get not {
_invertNext = !_invertNext;
return this;
}
/// Adds an equality expression to a query.
///
/// A query will only return objects where the selected property is equal to [value].
///
/// This method can be used on [int], [String], [bool], [double] and [DateTime] types.
///
/// If [value] is [String], the flag [caseSensitive] controls whether or not equality is case-sensitively compared.
///
/// Example:
///
/// final query = new Query<User>()
/// ..where((u) => u.id ).equalTo(1);
///
QueryExpressionJunction<T, InstanceType> equalTo(
T value, {
bool caseSensitive = true,
}) {
if (value is String) {
expression = StringExpression(
value,
PredicateStringOperator.equals,
caseSensitive: caseSensitive,
allowSpecialCharacters: false,
);
} else {
expression = ComparisonExpression(value, PredicateOperator.equalTo);
}
return _createJunction();
}
/// Adds a 'not equal' expression to a query.
///
/// A query will only return objects where the selected property is *not* equal to [value].
///
/// This method can be used on [int], [String], [bool], [double] and [DateTime] types.
///
/// If [value] is [String], the flag [caseSensitive] controls whether or not equality is case-sensitively compared.
///
/// Example:
///
/// final query = new Query<Employee>()
/// ..where((e) => e.id).notEqualTo(60000);
///
QueryExpressionJunction<T, InstanceType> notEqualTo(
T value, {
bool caseSensitive = true,
}) {
if (value is String) {
expression = StringExpression(
value,
PredicateStringOperator.equals,
caseSensitive: caseSensitive,
invertOperator: true,
allowSpecialCharacters: false,
);
} else {
expression = ComparisonExpression(value, PredicateOperator.notEqual);
}
return _createJunction();
}
/// Adds a 'like' expression to a query.
///
/// A query will only return objects where the selected property is like [value].
///
/// For more documentation on postgres pattern matching, see
/// https://www.postgresql.org/docs/10/functions-matching.html.
///
/// This method can be used on [String] types.
///
/// The flag [caseSensitive] controls whether strings are compared case-sensitively.
///
/// Example:
///
/// final query = new Query<User>()
/// ..where((u) => u.name ).like("bob");
///
QueryExpressionJunction<T, InstanceType> like(
String value, {
bool caseSensitive = true,
}) {
expression = StringExpression(
value,
PredicateStringOperator.equals,
caseSensitive: caseSensitive,
);
return _createJunction();
}
/// Adds a 'not like' expression to a query.
///
/// A query will only return objects where the selected property is *not* like [value].
///
/// For more documentation on postgres pattern matching, see
/// https://www.postgresql.org/docs/10/functions-matching.html.
///
/// This method can be used on [String] types.
///
/// The flag [caseSensitive] controls whether strings are compared case-sensitively.
///
/// Example:
///
/// final query = new Query<Employee>()
/// ..where((e) => e.id).notEqualTo(60000);
///
QueryExpressionJunction<T, InstanceType> notLike(
String value, {
bool caseSensitive = true,
}) {
expression = StringExpression(
value,
PredicateStringOperator.equals,
caseSensitive: caseSensitive,
invertOperator: true,
);
return _createJunction();
}
/// Adds a 'greater than' expression to a query.
///
/// A query will only return objects where the selected property is greater than [value].
///
/// This method can be used on [int], [String], [double] and [DateTime] types. For [DateTime] properties,
/// this method selects rows where the assigned property is 'later than' [value]. For [String] properties,
/// rows are selected if the value is alphabetically 'after' [value].
///
/// Example:
///
/// var query = new Query<Employee>()
/// ..where((e) => e.salary).greaterThan(60000);
QueryExpressionJunction<T, InstanceType> greaterThan(T value) {
expression = ComparisonExpression(value, PredicateOperator.greaterThan);
return _createJunction();
}
/// Adds a 'greater than or equal to' expression to a query.
///
/// A query will only return objects where the selected property is greater than [value].
///
/// This method can be used on [int], [String], [double] and [DateTime] types. For [DateTime] properties,
/// this method selects rows where the assigned property is 'later than or the same time as' [value]. For [String] properties,
/// rows are selected if the value is alphabetically 'after or the same as' [value].
/// Example:
///
/// var query = new Query<Employee>()
/// ..where((e) => e.salary).greaterThanEqualTo(60000);
QueryExpressionJunction<T, InstanceType> greaterThanEqualTo(T value) {
expression =
ComparisonExpression(value, PredicateOperator.greaterThanEqualTo);
return _createJunction();
}
/// Adds a 'less than' expression to a query.
///
/// A query will only return objects where the selected property is less than [value].
///
/// This method can be used on [int], [String], [double] and [DateTime] types. For [DateTime] properties,
/// this method selects rows where the assigned property is 'earlier than' [value]. For [String] properties,
/// rows are selected if the value is alphabetically 'before' [value].
/// Example:
///
/// var query = new Query<Employee>()
/// ..where((e) => e.salary).lessThan(60000);
QueryExpressionJunction<T, InstanceType> lessThan(T value) {
expression = ComparisonExpression(value, PredicateOperator.lessThan);
return _createJunction();
}
/// Adds a 'less than or equal to' expression to a query.
///
/// A query will only return objects where the selected property is less than or equal to [value].
///
/// This method can be used on [int], [String], [double] and [DateTime] types. For [DateTime] properties,
/// this method selects rows where the assigned property is 'earlier than or the same time as' [value]. For [String] properties,
/// rows are selected if the value is alphabetically 'before or the same as' [value].
/// Example:
///
/// var query = new Query<Employee>()
/// ..where((e) => e.salary).lessThanEqualTo(60000);
QueryExpressionJunction<T, InstanceType> lessThanEqualTo(T value) {
expression = ComparisonExpression(value, PredicateOperator.lessThanEqualTo);
return _createJunction();
}
/// Adds a 'contains string' expression to a query.
///
/// A query will only return objects where the selected property contains the string [value].
///
/// This method can be used on [String] types. The substring [value] must be found in the stored string.
/// The flag [caseSensitive] controls whether strings are compared case-sensitively.
///
/// Example:
///
/// var query = new Query<Employee>()
/// ..where((s) => s.title).contains("Director");
///
QueryExpressionJunction<T, InstanceType> contains(
String value, {
bool caseSensitive = true,
}) {
expression = StringExpression(
value,
PredicateStringOperator.contains,
caseSensitive: caseSensitive,
allowSpecialCharacters: false,
);
return _createJunction();
}
/// Adds a 'begins with string' expression to a query.
///
/// A query will only return objects where the selected property is begins with the string [value].
///
/// This method can be used on [String] types. The flag [caseSensitive] controls whether strings are compared case-sensitively.
///
/// Example:
///
/// var query = new Query<Employee>()
/// ..where((s) => s.name).beginsWith("B");
QueryExpressionJunction<T, InstanceType> beginsWith(
String value, {
bool caseSensitive = true,
}) {
expression = StringExpression(
value,
PredicateStringOperator.beginsWith,
caseSensitive: caseSensitive,
allowSpecialCharacters: false,
);
return _createJunction();
}
/// Adds a 'ends with string' expression to a query.
///
/// A query will only return objects where the selected property is ends with the string [value].
///
/// This method can be used on [String] types. The flag [caseSensitive] controls whether strings are compared case-sensitively.
///
/// Example:
///
/// var query = new Query<Employee>()
/// ..where((e) => e.name).endsWith("son");
QueryExpressionJunction<T, InstanceType> endsWith(
String value, {
bool caseSensitive = true,
}) {
expression = StringExpression(
value,
PredicateStringOperator.endsWith,
caseSensitive: caseSensitive,
allowSpecialCharacters: false,
);
return _createJunction();
}
/// Adds a 'equal to one of' expression to a query.
///
/// A query will only return objects where the selected property is equal to one of the [values].
///
/// This method can be used on [String], [int], [double], [bool] and [DateTime] types.
///
/// Example:
///
/// var query = new Query<Employee>()
/// ..where((e) => e.department).oneOf(["Engineering", "HR"]);
QueryExpressionJunction<T, InstanceType> oneOf(Iterable<T> values) {
if (values.isEmpty) {
throw ArgumentError(
"'Query.where.oneOf' cannot be the empty set or null.",
);
}
expression = SetMembershipExpression(values.toList());
return _createJunction();
}
/// Adds a 'between two values' expression to a query.
///
/// A query will only return objects where the selected property is between than [lhs] and [rhs].
///
/// This method can be used on [int], [String], [double] and [DateTime] types. For [DateTime] properties,
/// this method selects rows where the assigned property is 'later than' [lhs] and 'earlier than' [rhs]. For [String] properties,
/// rows are selected if the value is alphabetically 'after' [lhs] and 'before' [rhs].
///
/// Example:
///
/// var query = new Query<Employee>()
/// ..where((e) => e.salary).between(80000, 100000);
QueryExpressionJunction<T, InstanceType> between(T lhs, T rhs) {
expression = RangeExpression(lhs, rhs);
return _createJunction();
}
/// Adds a 'outside of the range crated by two values' expression to a query.
///
/// A query will only return objects where the selected property is not within the range established by [lhs] to [rhs].
///
/// This method can be used on [int], [String], [double] and [DateTime] types. For [DateTime] properties,
/// this method selects rows where the assigned property is 'later than' [rhs] and 'earlier than' [lhs]. For [String] properties,
/// rows are selected if the value is alphabetically 'before' [lhs] and 'after' [rhs].
///
/// Example:
///
/// var query = new Query<Employee>()
/// ..where((e) => e.salary).outsideOf(80000, 100000);
QueryExpressionJunction<T, InstanceType> outsideOf(T lhs, T rhs) {
expression = RangeExpression(lhs, rhs, within: false);
return _createJunction();
}
/// Adds an equality expression for foreign key columns to a query.
///
/// A query will only return objects where the selected object's primary key is equal to [identifier].
///
/// This method may only be used on belongs-to relationships; i.e., those that have a [Relate] annotation.
/// The type of [identifier] must match the primary key type of the selected object this expression is being applied to.
///
/// var q = new Query<Employee>()
/// ..where((e) => e.manager).identifiedBy(5);
QueryExpressionJunction<T, InstanceType> identifiedBy(dynamic identifier) {
expression = ComparisonExpression(identifier, PredicateOperator.equalTo);
return _createJunction();
}
/// Adds a 'null check' expression to a query.
///
/// A query will only return objects where the selected property is null.
///
/// This method can be applied to any property type.
///
/// Example:
///
/// var q = new Query<Employee>()
/// ..where((e) => e.manager).isNull();
QueryExpressionJunction<T, InstanceType> isNull() {
expression = const NullCheckExpression();
return _createJunction();
}
/// Adds a 'not null check' expression to a query.
///
/// A query will only return objects where the selected property is not null.
///
/// This method can be applied to any property type.
///
/// Example:
///
/// var q = new Query<Employee>()
/// ..where((e) => e.manager).isNotNull();
QueryExpressionJunction<T, InstanceType> isNotNull() {
expression = const NullCheckExpression(shouldBeNull: false);
return _createJunction();
}
}

View file

@ -1,423 +0,0 @@
/*
* 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';
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) {
_valueObject = entity.instanceOf() as InstanceType?;
_valueObject!.backing = ManagedBuilderBacking.from(
_valueObject!.entity,
_valueObject!.backing,
);
}
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) {
_valueObject = null;
return;
}
_valueObject = entity.instanceOf(
backing: ManagedBuilderBacking.from(entity, obj.backing),
);
}
/// 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,
) {
final properties = entity.identifyProperties(propertyIdentifier);
if (properties.length != 1) {
throw ArgumentError(
"Invalid property selector. Must reference a single property only.",
);
}
final expr = QueryExpression<T, InstanceType>(properties.first);
expressions.add(expr);
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,
ManagedSet<T>? Function(InstanceType x)? set,
}) {
final relationship = object ?? set!;
final desc = entity.identifyRelationship(relationship);
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,
QuerySortOrder order, {
T? boundingValue,
}) {
final attribute = entity.identifyAttribute(propertyIdentifier);
pageDescriptor =
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,
QuerySortOrder order,
) {
final attribute = entity.identifyAttribute(propertyIdentifier);
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,
) {
final properties = entity.identifyProperties(propertyIdentifiers);
if (properties.any(
(kp) => kp.path.any(
(p) =>
p is ManagedRelationshipDescription &&
p.relationshipType != ManagedRelationshipType.belongsTo,
),
)) {
throw ArgumentError(
"Invalid property selector. Cannot select has-many or has-one relationship properties. Use join instead.",
);
}
_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) {
values.willInsert();
} else if (op == Validating.update) {
values.willUpdate();
}
final ctx = values.validate(forEvent: op);
if (!ctx.isValid) {
throw ValidationException(ctx.errors);
}
}
}
/// 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,
) {
if (subQueries.containsKey(fromRelationship)) {
throw StateError(
"Invalid query. Cannot join same property more than once.",
);
}
// Ensure we don't cyclically join
var parent = _parentQuery;
while (parent != null) {
if (parent.subQueries.containsKey(fromRelationship.inverse)) {
final validJoins = fromRelationship.entity.relationships.values
.where((r) => !identical(r, fromRelationship))
.map((r) => "'${r!.name}'")
.join(", ");
throw StateError(
"Invalid query construction. This query joins '${fromRelationship.entity.tableName}' "
"with '${fromRelationship.inverse!.entity.tableName}' on property '${fromRelationship.name}'. "
"However, '${fromRelationship.inverse!.entity.tableName}' "
"has also joined '${fromRelationship.entity.tableName}' on this property's inverse "
"'${fromRelationship.inverse!.name}' earlier in the 'Query'. "
"Perhaps you meant to join on another property, such as: $validJoins?");
}
parent = parent._parentQuery;
}
final subquery = Query<T>(context);
(subquery as QueryMixin)._parentQuery = this;
subQueries[fromRelationship] = subquery;
return subquery;
}
}

View file

@ -1,49 +0,0 @@
/*
* 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].
///
/// [QueryPage]s are a convenient way of accomplishing paging through a large
/// set of values. A page has the property to page on, the order in which the table is being
/// paged and a value that indicates where in the ordered list of results the paging should start from.
///
/// Paging conceptually works by putting all of the rows in a table into an order. This order is determined by
/// applying [order] to the values of [propertyName]. Once this order is defined, the position in that ordered list
/// is found by going to the row (or rows) where [boundingValue] is eclipsed. That is, the point where row N
/// has a value for [propertyName] that is less than or equal to [boundingValue] and row N + 1 has a value that is greater than
/// [boundingValue]. The rows returned will start at row N + 1, ignoring rows 0 - N.
///
/// A query page should be used in conjunction with [Query.fetchLimit].
class QueryPage {
QueryPage(this.order, this.propertyName, {this.boundingValue});
/// 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.
QuerySortOrder order;
/// 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 operators like '<' and '>'.
String propertyName;
/// The point within an ordered set of result values in which rows will begin being fetched from.
///
/// After the table has been ordered by its [propertyName] and [order], the point in that ordered table
/// is found where a row goes from being less than or equal to this value to greater than or equal to this value.
/// Page results start at the row where this comparison changes.
///
/// Rows with a value equal to this value are not included in the data set. This value may be null. When this value is null,
/// the [boundingValue] is set to be the just outside the first or last element of the ordered database table, depending on the direction.
/// This allows for query pages that fetch the first or last page of elements when the starting/ending value is not known.
dynamic boundingValue;
}

View file

@ -1,459 +0,0 @@
/*
* 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';
/// 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});
class QueryPredicate {
/// Default constructor for [QueryPredicate].
///
/// The [format] and [parameters] of this predicate. [parameters] may be null.
QueryPredicate(this.format, [this.parameters = const {}]);
/// Creates an empty [QueryPredicate] instance.
///
/// The format string is the empty string and parameters is the empty map.
QueryPredicate.empty()
: format = "",
parameters = {};
/// Combines [predicates] with 'AND' keyword.
///
/// 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.
///
/// 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();
}
if (predicateList.length == 1) {
return predicateList.first;
}
/// 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))
.toList();
if (duplicateKeys.isNotEmpty) {
var fmt = predicate.format;
final Map<String, String> dupeMap = {};
for (final key in duplicateKeys) {
final replacementKey = "$key$dupeCounter";
fmt = fmt.replaceAll("@$key", "@$replacementKey");
dupeMap[key] = replacementKey;
dupeCounter++;
}
allFormatStrings.add(fmt);
predicate.parameters.forEach((key, value) {
valueMap[dupeMap[key] ?? key] = value;
});
} else {
allFormatStrings.add(predicate.format);
valueMap.addAll(predicate.parameters);
}
}
final predicateFormat = "(${allFormatStrings.join(" AND ")})";
return QueryPredicate(predicateFormat, valueMap);
}
/// 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.
String format;
/// A map of values to replace in the format string at execution time.
///
/// 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 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,
notEqual,
lessThanEqualTo,
greaterThanEqualTo,
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:
return PredicateOperator.greaterThanEqualTo;
case PredicateOperator.greaterThan:
return PredicateOperator.lessThanEqualTo;
case PredicateOperator.notEqual:
return PredicateOperator.equalTo;
case PredicateOperator.lessThanEqualTo:
return PredicateOperator.greaterThan;
case PredicateOperator.greaterThanEqualTo:
return PredicateOperator.lessThan;
case PredicateOperator.equalTo:
return PredicateOperator.notEqual;
}
}
}
/// 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, {
this.caseSensitive = true,
this.invertOperator = false,
this.allowSpecialCharacters = true,
});
/// 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(
value,
operator,
caseSensitive: caseSensitive,
invertOperator: !invertOperator,
allowSpecialCharacters: allowSpecialCharacters,
);
}
}

View file

@ -1,434 +0,0 @@
/*
* 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';
import 'package:protevus_database/src/query/error.dart';
import 'package:protevus_database/src/query/matcher_expression.dart';
import 'package:protevus_database/src/query/predicate.dart';
import 'package:protevus_database/src/query/reduce.dart';
import 'package:protevus_database/src/query/sort_predicate.dart';
export 'error.dart';
export 'matcher_expression.dart';
export 'mixin.dart';
export 'predicate.dart';
export 'reduce.dart';
export 'sort_predicate.dart';
/// An object for configuring and executing a database query.
///
/// Queries are used to fetch, update, insert, delete and count [InstanceType]s in a database.
/// [InstanceType] must be a [ManagedObject].
///
/// final query = Query<Employee>()
/// ..where((e) => e.salary).greaterThan(50000);
/// final employees = await query.fetch();
abstract class Query<InstanceType extends ManagedObject> {
/// Creates a new [Query].
///
/// The query will be sent to the database described by [context].
/// For insert or update queries, you may provide [values] through this constructor
/// or set the field of the same name later. If set in the constructor,
/// [InstanceType] is inferred.
factory Query(ManagedContext context, {InstanceType? values}) {
final entity = context.dataModel!.tryEntityForType(InstanceType);
if (entity == null) {
throw ArgumentError(
"Invalid context. The data model of 'context' does not contain '$InstanceType'.",
);
}
return context.persistentStore.newQuery<InstanceType>(
context,
entity,
values: values,
);
}
/// Creates a new [Query] without a static type.
///
/// This method is used when generating queries dynamically from runtime values,
/// where the static type argument cannot be defined. Behaves just like the unnamed constructor.
///
/// If [entity] is not in [context]'s [ManagedContext.dataModel], throws a internal failure [QueryException].
factory Query.forEntity(ManagedEntity entity, ManagedContext context) {
if (!context.dataModel!.entities.any((e) => identical(entity, e))) {
throw StateError(
"Invalid query construction. Entity for '${entity.tableName}' is from different context than specified for query.",
);
}
return context.persistentStore.newQuery<InstanceType>(context, entity);
}
/// Inserts a single [object] into the database managed by [context].
///
/// This is equivalent to creating a [Query], assigning [object] to [values], and invoking [insert].
static Future<T> insertObject<T extends ManagedObject>(
ManagedContext context,
T object,
) {
return context.insertObject(object);
}
/// Inserts each object in [objects] into the database managed by [context] in a single transaction.
///
/// This currently has no Query instance equivalent
static Future<List<T>> insertObjects<T extends ManagedObject>(
ManagedContext context,
List<T> objects,
) async {
return context.insertObjects(objects);
}
/// Configures this instance to fetch a relationship property identified by [object] or [set].
///
/// By default, objects returned by [Query.fetch] do not have their relationship properties populated. (In other words,
/// [ManagedObject] and [ManagedSet] properties are null.) This method configures this instance to conduct a SQL join,
/// allowing it to fetch relationship properties for the returned instances.
///
/// Consider a [ManagedObject] subclass with the following relationship properties as an example:
///
/// class User extends ManagedObject<_User> implements _User {}
/// class _User {
/// Profile profile;
/// ManagedSet<Note> notes;
/// }
///
/// To fetch an object and one of its has-one properties, use the [object] closure:
///
/// var query = Query<User>()
/// ..join(object: (u) => u.profile);
///
/// To fetch an object and its has-many properties, use the [set] closure:
///
/// var query = Query<User>()
/// ..join(set: (u) => u.notes);
///
/// Both [object] and [set] are passed an empty instance of the type being queried. [object] must return a has-one property (a [ManagedObject] subclass)
/// of the object it is passed. [set] must return a has-many property (a [ManagedSet]) of the object it is passed.
///
/// Multiple relationship properties can be included by invoking this method multiple times with different properties, e.g.:
///
/// var query = Query<User>()
/// ..join(object: (u) => u.profile)
/// ..join(set: (u) => u.notes);
///
/// This method also returns a new instance of [Query], where [InstanceType] is is the type of the relationship property. This can be used
/// to configure which properties are returned for the related objects and to filter a [ManagedSet] relationship property. For example:
///
/// var query = Query<User>();
/// var subquery = query.join(set: (u) => u.notes)
/// ..where.dateCreatedAt = whereGreaterThan(someDate);
///
/// This mechanism only works on [fetch] and [fetchOne] execution methods. You *must not* execute a subquery created by this method.
Query<T> join<T extends ManagedObject>({
T? Function(InstanceType x)? object,
ManagedSet<T>? Function(InstanceType x)? set,
});
/// Configures this instance to fetch a section of a larger result set.
///
/// This method provides an effective mechanism for paging a result set by ordering rows
/// by some property, offsetting into that ordered set and returning rows starting from that offset.
/// The [fetchLimit] of this instance must also be configured to limit the size of the page.
///
/// The property that determines order is identified by [propertyIdentifier]. This closure must
/// return a property of of [InstanceType]. The order is determined by [order]. When fetching
/// the 'first' page of results, no value is passed for [boundingValue]. As later pages are fetched,
/// the value of the paging property for the last returned object in the previous result set is used
/// as [boundingValue]. For example:
///
/// var recentHireQuery = Query<Employee>()
/// ..pageBy((e) => e.hireDate, QuerySortOrder.descending);
/// var recentHires = await recentHireQuery.fetch();
///
/// var nextRecentHireQuery = Query<Employee>()
/// ..pageBy((e) => e.hireDate, QuerySortOrder.descending,
/// boundingValue: recentHires.last.hireDate);
///
/// Note that internally, [pageBy] adds a matcher to [where] and adds a high-priority [sortBy].
/// Adding multiple [pageBy]s to an instance has undefined behavior.
void pageBy<T>(
T Function(InstanceType x) propertyIdentifier,
QuerySortOrder order, {
T? boundingValue,
});
/// Configures this instance to sort its results by some property and order.
///
/// This method will have the database perform a sort by some property identified by [propertyIdentifier].
/// [propertyIdentifier] must return a scalar property of [InstanceType] that can be compared. The [order]
/// indicates the order the returned rows will be in. Multiple [sortBy]s may be invoked on an instance;
/// the order in which they are added indicates sort precedence. Example:
///
/// var query = Query<Employee>()
/// ..sortBy((e) => e.name, QuerySortOrder.ascending);
void sortBy<T>(
T Function(InstanceType x) propertyIdentifier,
QuerySortOrder order,
);
/// The [ManagedEntity] of the [InstanceType].
ManagedEntity get entity;
/// The [ManagedContext] this query will be executed on.
ManagedContext get context;
/// Returns a new object that can execute functions like sum, average, maximum, etc.
///
/// The methods of this object will execute an aggregate function on the database table.
/// For example, this property can be used to find the average age of all users.
///
/// var query = Query<User>();
/// var averageAge = await query.reduce.average((user) => user.age);
///
/// Any where clauses established by [where] or [predicate] will impact the rows evaluated
/// and therefore the value returned from this object's instance methods.
///
/// Always returns a new instance of [QueryReduceOperation]. The returned object is permanently
/// associated with this instance. Any changes to this instance (i.e., modifying [where]) will impact the
/// result.
QueryReduceOperation<InstanceType> get reduce;
/// Selects a property from the object being queried to add a filtering expression.
///
/// You use this property to add filtering expression to a query. The expressions are added to the SQL WHERE clause
/// of the generated query.
///
/// You provide a closure for [propertyIdentifier] that returns a property of its argument. Its argument is always
/// an empty instance of the object being queried. You invoke methods like [QueryExpression.lessThan] on the
/// object returned from this method to add an expression to this query.
///
/// final query = Query<Employee>()
/// ..where((e) => e.name).equalTo("Bob");
///
/// You may select properties of relationships using this method.
///
/// final query = Query<Employee>()
/// ..where((e) => e.manager.name).equalTo("Sally");
///
QueryExpression<T, InstanceType> where<T>(
T Function(InstanceType x) propertyIdentifier,
);
/// Confirms that a query has no predicate before executing it.
///
/// This is a safety measure for update and delete queries to prevent accidentally updating or deleting every row.
/// This flag defaults to false, meaning that if this query is either an update or a delete, but contains no predicate,
/// it will fail. If a query is meant to update or delete every row on a table, you may set this to true to allow this query to proceed.
bool canModifyAllInstances = false;
/// Number of seconds before a Query times out.
///
/// A Query will fail and throw a [QueryException] if [timeoutInSeconds] seconds elapse before the query completes.
/// Defaults to 30 seconds.
int timeoutInSeconds = 30;
/// Limits the number of objects returned from the Query.
///
/// Defaults to 0. When zero, there is no limit to the number of objects returned from the Query.
/// This value should be set when using [pageBy] to limit the page size.
int fetchLimit = 0;
/// Offsets the rows returned.
///
/// The set of rows returned will exclude the first [offset] number of rows selected in the query. Do not
/// set this property when using [pageBy].
int offset = 0;
/// A predicate for filtering the result or operation set.
///
/// A predicate will identify the rows being accessed, see [QueryPredicate] for more details. Prefer to use
/// [where] instead of this property directly.
QueryPredicate? predicate;
/// A predicate for sorting the result.
///
/// A predicate will identify the rows being accessed, see [QuerySortPredicate] for more details. Prefer to use
/// [sortBy] instead of this property directly.
QuerySortPredicate? sortPredicate;
/// Values to be used when inserting or updating an object.
///
/// This method is an unsafe version of [values]. Prefer to use [values] instead.
/// Keys in this map must be the name of a property of [InstanceType], otherwise an exception
/// is thrown. Values provided in this map are not run through any [Validate] annotations
/// declared by the [InstanceType].
///
/// Do not set this property and [values] on the same query. If both this property and [values] are set,
/// the behavior is undefined.
Map<String, dynamic>? valueMap;
/// Values to be sent to database during an [update] or [insert] query.
///
/// You set values for the properties of this object to insert a row or update one or more rows.
/// This property is the same type as the type being inserted or updated. [values] is empty (but not null)
/// when a [Query] is first created, therefore, you do not have to assign an instance to it and may set
/// values for its properties immediately:
///
/// var q = Query<User>()
/// ..values.name = 'Joe'
/// ..values.job = 'programmer';
/// await q.insert();
///
/// You may only set values for properties that are backed by a column. This includes most properties, except
/// all [ManagedSet] properties and [ManagedObject] properties that do not have a [Relate] annotation. If you attempt
/// to set a property that isn't allowed on [values], an error is thrown.
///
/// If a property of [values] is a [ManagedObject] with a [Relate] annotation,
/// you may provide a value for its primary key property. This value will be
/// stored in the foreign key column that backs the property. You may set
/// properties of this type immediately, without having to create an instance
/// of the related type:
///
/// // Assumes that Employee is declared with the following property:
/// // @Relate(#employees)
/// // Manager manager;
///
/// final q = Query<Employee>()
/// ..values.name = "Sally"
/// ..values.manager.id = 10;
/// await q.insert();
///
/// WARNING: You may replace this property with a new instance of [InstanceType].
/// When doing so, a copy of the object is created and assigned to this property.
///
/// final o = SomeObject()
/// ..id = 1;
/// final q = Query<SomeObject>()
/// ..values = o;
///
/// o.id = 2;
/// assert(q.values.id == 1); // true
///
late InstanceType values;
/// Configures the list of properties to be fetched for [InstanceType].
///
/// This method configures which properties will be populated for [InstanceType] when returned
/// from this query. This impacts all query execution methods that return [InstanceType] or [List] of [InstanceType].
///
/// The following example would configure this instance to fetch the 'id' and 'name' for each returned 'Employee':
///
/// var q = Query<Employee>()
/// ..returningProperties((employee) => [employee.id, employee.name]);
///
/// Note that if the primary key property of an object is omitted from this list, it will be added when this
/// instance executes. If the primary key value should not be sent back as part of an API response,
/// it can be stripped from the returned object(s) with [ManagedObject.removePropertyFromBackingMap].
///
/// If this method is not invoked, the properties defined by [ManagedEntity.defaultProperties] are returned.
void returningProperties(
List<dynamic> Function(InstanceType x) propertyIdentifiers,
);
/// Inserts an [InstanceType] into the underlying database.
///
/// The [Query] must have its [values] or [valueMap] property set. This operation will
/// insert a row with the data supplied in those fields to the database in [context]. The return value is
/// a [Future] that completes with the newly inserted [InstanceType]. Example:
///
/// var q = Query<User>()
/// ..values.name = "Joe";
/// var newUser = await q.insert();
///
/// If the [InstanceType] has properties with [Validate] metadata, those validations
/// will be executed prior to sending the query to the database.
///
/// The method guaranties that exactly one row will be inserted and returned
/// or an exception will be thrown and the row will not be written to the database.
Future<InstanceType> insert();
/// Inserts an [InstanceType]s into the underlying database.
///
/// The [Query] must not have its [values] nor [valueMap] property set. This
/// operation will insert a row for each item in [objects] to the database in
/// [context]. The return value is a [Future] that completes with the newly
/// inserted [InstanceType]s. Example:
///
/// final users = [
/// User()..email = 'user1@example.dev',
/// User()..email = 'user2@example.dev',
/// ];
/// final q = Query<User>();
/// var newUsers = await q.insertMany(users);
///
/// If the [InstanceType] has properties with [Validate] metadata, those
/// validations will be executed prior to sending the query to the database.
///
/// The method guaranties that either all rows will be inserted and returned
/// or exception will be thrown and non of the rows will be written to the database.
Future<List<InstanceType>> insertMany(List<InstanceType> objects);
/// Updates [InstanceType]s in the underlying database.
///
/// The [Query] must have its [values] or [valueMap] property set and should likely have its [predicate] or [where] set as well. This operation will
/// update each row that matches the conditions in [predicate]/[where] with the values from [values] or [valueMap]. If no [where] or [predicate] is set,
/// this method will throw an exception by default, assuming that you don't typically want to update every row in a database table. To specify otherwise,
/// set [canModifyAllInstances] to true.
/// The return value is a [Future] that completes with the any updated [InstanceType]s. Example:
///
/// var q = Query<User>()
/// ..where.name = "Fred"
/// ..values.name = "Joe";
/// var usersNamedFredNowNamedJoe = await q.update();
///
/// If the [InstanceType] has properties with [Validate] metadata, those validations
/// will be executed prior to sending the query to the database.
Future<List<InstanceType>> update();
/// Updates an [InstanceType] in the underlying database.
///
/// This method works the same as [update], except it may only update one row in the underlying database. If this method
/// ends up modifying multiple rows, an exception is thrown.
///
/// If the [InstanceType] has properties with [Validate] metadata, those validations
/// will be executed prior to sending the query to the database.
Future<InstanceType?> updateOne();
/// Fetches [InstanceType]s from the database.
///
/// This operation will return all [InstanceType]s from the database, filtered by [predicate]/[where]. Example:
///
/// var q = Query<User>();
/// var allUsers = q.fetch();
///
Future<List<InstanceType>> fetch();
/// Fetches a single [InstanceType] from the database.
///
/// This method behaves the same as [fetch], but limits the results to a single object.
Future<InstanceType?> fetchOne();
/// Deletes [InstanceType]s from the underlying database.
///
/// This method will delete rows identified by [predicate]/[where]. If no [where] or [predicate] is set,
/// this method will throw an exception by default, assuming that you don't typically want to delete every row in a database table. To specify otherwise,
/// set [canModifyAllInstances] to true.
///
/// This method will return the number of rows deleted.
/// Example:
///
/// var q = Query<User>()
/// ..where.id = whereEqualTo(1);
/// var deletedCount = await q.delete();
Future<int> delete();
}
/// Order value for [Query.pageBy] and [Query.sortBy].
enum QuerySortOrder {
/// Ascending order. Example: 1, 2, 3, 4, ...
ascending,
/// Descending order. Example: 4, 3, 2, 1, ...
descending
}

View file

@ -1,71 +0,0 @@
/*
* 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';
/// Executes aggregate functions like average, count, sum, etc.
///
/// See instance methods for available aggregate functions.
///
/// See [Query.reduce] for more details on usage.
abstract class QueryReduceOperation<T extends ManagedObject> {
/// Computes the average of some [ManagedObject] property.
///
/// [selector] identifies the property being averaged, e.g.
///
/// var query = Query<User>();
/// var averageAge = await query.reduce.average((user) => user.age);
///
/// The property must be an attribute and its type must be an [num], i.e. [int] or [double].
Future<double?> average(num? Function(T object) selector);
/// Counts the number of [ManagedObject] instances in the database.
///
/// Note: this can be an expensive query. Consult the documentation
/// for the underlying database.
///
/// Example:
///
/// var query = Query<User>();
/// var totalUsers = await query.reduce.count();
///
Future<int> count();
/// Finds the maximum of some [ManagedObject] property.
///
/// [selector] identifies the property being evaluated, e.g.
///
/// var query = Query<User>();
/// var oldestUser = await query.reduce.maximum((user) => user.age);
///
/// The property must be an attribute and its type must be [String], [int], [double], or [DateTime].
Future<U?> maximum<U>(U? Function(T object) selector);
/// Finds the minimum of some [ManagedObject] property.
///
/// [selector] identifies the property being evaluated, e.g.
///
/// var query = new Query<User>();
/// var youngestUser = await query.reduce.minimum((user) => user.age);
///
/// The property must be an attribute and its type must be [String], [int], [double], or [DateTime].
Future<U?> minimum<U>(U? Function(T object) selector);
/// Finds the sum of some [ManagedObject] property.
///
/// [selector] identifies the property being evaluated, e.g.
///
/// var query = new Query<User>();
/// var yearsLivesByAllUsers = await query.reduce.sum((user) => user.age);
///
/// The property must be an attribute and its type must be an [num], i.e. [int] or [double].
Future<U?> sum<U extends num>(U? Function(T object) selector);
}

View file

@ -1,33 +0,0 @@
/*
* 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.
String key;
/// 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,35 +0,0 @@
/*
* 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';
/// 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,
);
/// The name of a property to sort by.
String predicate;
/// The order in which values should be sorted.
///
/// 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,125 +0,0 @@
/*
* 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 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;
}
/// The base class for migration instructions.
///
/// For each set of changes to a database, a subclass of [Migration] is created.
/// Subclasses will override [upgrade] to make changes to the [Schema] which
/// are translated into database operations to update a database's schema.
abstract class Migration {
/// The current state of the [Schema].
///
/// During migration, this value will be modified as [SchemaBuilder] operations
/// are executed. See [SchemaBuilder].
Schema get currentSchema => database.schema;
/// The [PersistentStore] that represents the database being migrated.
PersistentStore? get store => database.store;
// This value is provided by the 'upgrade' tool and is derived from the filename.
int? version;
/// Receiver for database altering operations.
///
/// Methods invoked on this instance - such as [SchemaBuilder.createTable] - will be validated
/// and generate the appropriate SQL commands to apply to a database to alter its schema.
late SchemaBuilder database;
/// Method invoked to upgrade a database to this migration version.
///
/// Subclasses will override this method and invoke methods on [database] to upgrade
/// the database represented by [store].
Future upgrade();
/// Method invoked to downgrade a database from this migration version.
///
/// Subclasses will override this method and invoke methods on [database] to downgrade
/// the database represented by [store].
Future downgrade();
/// Method invoked to seed a database's data after this migration version is upgraded to.
///
/// Subclasses will override this method and invoke query methods on [store] to add data
/// 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,
int? version, {
List<String>? changeList,
}) {
final diff = existingSchema.differenceFrom(newSchema);
final source =
SchemaBuilder.fromDifference(null, diff, changeList: changeList)
.commands
.map((line) => "\t\t$line")
.join("\n");
return """
import 'dart:async';
import 'package:protevus_database/prots_database.dart
class Migration$version extends Migration {
@override
Future upgrade() async {
$source
}
@override
Future downgrade() async {}
@override
Future seed() async {}
}
""";
}
}

View file

@ -1,243 +0,0 @@
/*
* 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';
export 'migration.dart';
export 'schema_builder.dart';
export 'schema_column.dart';
export 'schema_table.dart';
/// A portable representation of a database schema.
///
/// Instances of this type contain the database-only details of a [ManagedDataModel] and are typically
/// instantiated from [ManagedDataModel]s. The purpose of this type is to have a portable, differentiable representation
/// of an application's data model for use in external tooling.
class Schema {
/// Creates an instance of this type with a specific set of [tables].
///
/// Prefer to use [Schema.fromDataModel].
Schema(List<SchemaTable> tables) : _tableStorage = tables;
/// Creates an instance of this type from [dataModel].
///
/// This is preferred method of creating an instance of this type. Each [ManagedEntity]
/// in [dataModel] will correspond to a [SchemaTable] in [tables].
Schema.fromDataModel(ManagedDataModel dataModel) {
_tables = dataModel.entities.map((e) => SchemaTable.fromEntity(e)).toList();
}
/// Creates a deep copy of [otherSchema].
Schema.from(Schema otherSchema) {
_tables =
otherSchema.tables.map((table) => SchemaTable.from(table)).toList();
}
/// Creates a instance of this type from [map].
///
/// [map] is typically created from [asMap].
Schema.fromMap(Map<String, dynamic> map) {
_tables = (map["tables"] as List<Map<String, dynamic>>)
.map((t) => SchemaTable.fromMap(t))
.toList();
}
/// Creates an empty schema.
Schema.empty() {
_tables = [];
}
/// The tables in this database.
///
/// Returns an immutable list of tables in this schema.
List<SchemaTable> get tables => List.unmodifiable(_tableStorage);
// 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) {
t.schema = this;
}
}
/// Gets a table from [tables] by that table's name.
///
/// See [tableForName] for details.
SchemaTable? operator [](String tableName) => tableForName(tableName);
/// The differences between two schemas.
///
/// In the return value, the receiver is the [SchemaDifference.expectedSchema]
/// and [otherSchema] is [SchemaDifference.actualSchema].
SchemaDifference differenceFrom(Schema otherSchema) {
return SchemaDifference(this, otherSchema);
}
/// Adds a table to this instance.
///
/// Sets [table]'s [SchemaTable.schema] to this instance.
void addTable(SchemaTable table) {
if (this[table.name!] != null) {
throw SchemaException(
"Table ${table.name} already exists and cannot be added.",
);
}
_tableStorage.add(table);
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(
"Table ${existingTable.name} does not exist and cannot be replaced.",
);
}
final index = _tableStorage.indexOf(existingTable);
_tableStorage[index] = newTable;
newTable.schema = this;
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!");
}
/// Removes a table from this instance.
///
/// [table] must be an instance in [tables] or an exception is thrown.
/// Sets [table]'s [SchemaTable.schema] to null.
void removeTable(SchemaTable table) {
if (!tables.contains(table)) {
throw SchemaException("Table ${table.name} does not exist in schema.");
}
table.schema = null;
_tableStorage.remove(table);
}
/// Returns a [SchemaTable] for [name].
///
/// [name] is case-insensitively compared to every [SchemaTable.name]
/// in [tables]. If no table with this name exists, null is returned.
///
/// Note: tables are typically prefixed with an underscore when using
/// Conduit naming conventions for [ManagedObject].
SchemaTable? tableForName(String name) {
final lowercaseName = name.toLowerCase();
return tables
.firstWhereOrNull((t) => t.name!.toLowerCase() == lowercaseName);
}
/// Emits this instance as a transportable [Map].
Map<String, dynamic> asMap() {
return {"tables": tables.map((t) => t.asMap()).toList()};
}
}
/// The difference between two compared [Schema]s.
///
/// 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!];
if (actualTable == null) {
_differingTables.add(SchemaTableDifference(expectedTable, null));
} else {
final diff = expectedTable.differenceFrom(actualTable);
if (diff.hasDifferences) {
_differingTables.add(diff);
}
}
}
_differingTables.addAll(
actualSchema.tables
.where((t) => expectedSchema[t.name!] == null)
.map((unexpectedTable) {
return SchemaTableDifference(null, unexpectedTable);
}),
);
}
/// The 'expected' schema.
final Schema expectedSchema;
/// The 'actual' schema.
final Schema actualSchema;
/// Whether or not [expectedSchema] and [actualSchema] have differences.
///
/// If false, both [expectedSchema] and [actualSchema], their tables, and those tables' columns are identical.
bool get hasDifferences => _differingTables.isNotEmpty;
/// Human-readable messages to describe differences between [expectedSchema] and [actualSchema].
///
/// Empty is [hasDifferences] is false.
List<String> get errorMessages =>
_differingTables.expand((diff) => diff.errorMessages).toList();
/// 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)
.map((d) => d.actualTable)
.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)
.map((diff) => diff.expectedTable)
.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,734 +0,0 @@
/*
* 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';
/*
Tests for this class are spread out some. The testing concept used starts by understanding that
that each method invoked on the builder (e.g. createTable, addColumn) adds a statement to [commands].
A statement is either:
a) A Dart statement that replicate the command to build migration code
b) A SQL command when running a migration
In effect, the generated Dart statement is the source code for the invoked method. Each method invoked on a
builder is tested so that the generated Dart code is equivalent
to the invocation. These tests are in generate_code_test.dart.
The code to ensure the generated SQL is accurate is in db/postgresql/schema_generator_sql_mapping_test.dart.
The logic that goes into testing that the commands generated to build a valid schema in an actual postgresql are in db/postgresql/migration_test.dart.
*/
/// Generates SQL or Dart code that modifies a database schema.
class SchemaBuilder {
/// 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.
SchemaBuilder(this.store, this.inputSchema, {this.isTemporary = false}) {
schema = Schema.from(inputSchema);
}
/// Creates a builder starting from the empty schema.
///
/// If [store] is null, this builder will emit [commands] that are Dart statements that replicate the methods invoked on this object.
/// Otherwise, [commands] are SQL commands (for the database represented by [store]) that are equivalent to the method invoked on this object.
///
/// 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, {
bool isTemporary = false,
List<String>? changeList,
}) : this.fromDifference(
store,
SchemaDifference(Schema.empty(), targetSchema),
isTemporary: isTemporary,
changeList: changeList,
);
/// 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, {
this.isTemporary = false,
List<String>? changeList,
}) {
schema = difference.expectedSchema;
_generateSchemaCommands(
difference,
changeList: changeList,
temporary: isTemporary,
);
}
/// 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.
///
/// If this value is not-null, [commands] is a list of SQL commands for the underlying database that change the schema in response to
/// methods invoked on this object. If this value is null, [commands] is a list Dart statements that replicate the methods invoked on this object.
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.
///
/// If [store] is non-null, these commands will be SQL commands that upgrade [inputSchema] to [schema] as determined by [store].
/// If [store] is null, these commands are ;-terminated Dart expressions that replicate the methods to call on this object to upgrade [inputSchema] to [schema].
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);
if (store != null) {
commands.addAll(store!.createTable(table, isTemporary: isTemporary));
} else {
commands.add(_getNewTableExpression(table));
}
}
/// 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) {
throw SchemaException("Table $currentTableName does not exist.");
}
schema.renameTable(table, newName);
if (store != null) {
commands.addAll(store!.renameTable(table, newName));
} else {
commands.add("database.renameTable('$currentTableName', '$newName');");
}
}
/// 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) {
throw SchemaException("Table $tableName does not exist.");
}
schema.removeTable(table);
if (store != null) {
commands.addAll(store!.deleteTable(table));
} else {
commands.add('database.deleteTable("$tableName");');
}
}
/// 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,
) {
final existingTable = schema.tableForName(tableName);
if (existingTable == null) {
throw SchemaException("Table $tableName does not exist.");
}
final newTable = SchemaTable.from(existingTable);
modify(newTable);
schema.replaceTable(existingTable, newTable);
final shouldAddUnique = existingTable.uniqueColumnSet == null &&
newTable.uniqueColumnSet != null;
final shouldRemoveUnique = existingTable.uniqueColumnSet != null &&
newTable.uniqueColumnSet == null;
final innerCommands = <String>[];
if (shouldAddUnique) {
if (store != null) {
commands.addAll(store!.addTableUniqueColumnSet(newTable));
} else {
innerCommands.add(
"t.uniqueColumnSet = [${newTable.uniqueColumnSet!.map((s) => '"$s"').join(',')}]",
);
}
} else if (shouldRemoveUnique) {
if (store != null) {
commands.addAll(store!.deleteTableUniqueColumnSet(newTable));
} else {
innerCommands.add("t.uniqueColumnSet = null");
}
} else {
final haveSameLength = existingTable.uniqueColumnSet!.length ==
newTable.uniqueColumnSet!.length;
final haveSameKeys = existingTable.uniqueColumnSet!
.every((s) => newTable.uniqueColumnSet!.contains(s));
if (!haveSameKeys || !haveSameLength) {
if (store != null) {
commands.addAll(store!.deleteTableUniqueColumnSet(newTable));
commands.addAll(store!.addTableUniqueColumnSet(newTable));
} else {
innerCommands.add(
"t.uniqueColumnSet = [${newTable.uniqueColumnSet!.map((s) => '"$s"').join(',')}]",
);
}
}
}
if (store == null && innerCommands.isNotEmpty) {
commands.add(
'database.alterTable("$tableName", (t) {${innerCommands.join(";")};});',
);
}
}
/// 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, {
String? unencodedInitialValue,
}) {
final table = schema.tableForName(tableName);
if (table == null) {
throw SchemaException("Table $tableName does not exist.");
}
table.addColumn(column);
if (store != null) {
commands.addAll(
store!.addColumn(
table,
column,
unencodedInitialValue: unencodedInitialValue,
),
);
} else {
commands.add(
'database.addColumn("${column.table!.name}", ${_getNewColumnExpression(column)});',
);
}
}
/// 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) {
throw SchemaException("Table $tableName does not exist.");
}
final column = table.columnForName(columnName);
if (column == null) {
throw SchemaException("Column $columnName does not exists.");
}
table.removeColumn(column);
if (store != null) {
commands.addAll(store!.deleteColumn(table, column));
} else {
commands.add('database.deleteColumn("$tableName", "$columnName");');
}
}
/// 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) {
throw SchemaException("Table $tableName does not exist.");
}
final column = table.columnForName(columnName);
if (column == null) {
throw SchemaException("Column $columnName does not exists.");
}
table.renameColumn(column, newName);
if (store != null) {
commands.addAll(store!.renameColumn(table, column, newName));
} else {
commands.add(
"database.renameColumn('$tableName', '$columnName', '$newName');",
);
}
}
/// Validates and alters a column in a table in [schema].
///
/// Alterations are made by setting properties of the column passed to [modify]. If the column's nullability
/// changes from nullable to not nullable, all previously null values for that column
/// are set to the value of [unencodedInitialValue].
///
/// Example:
///
/// database.alterColumn("table", "column", (c) {
/// c.isIndexed = true;
/// c.isNullable = false;
/// }), unencodedInitialValue: "0");
void alterColumn(
String tableName,
String columnName,
void Function(SchemaColumn targetColumn) modify, {
String? unencodedInitialValue,
}) {
final table = schema.tableForName(tableName);
if (table == null) {
throw SchemaException("Table $tableName does not exist.");
}
final existingColumn = table[columnName];
if (existingColumn == null) {
throw SchemaException("Column $columnName does not exist.");
}
final newColumn = SchemaColumn.from(existingColumn);
modify(newColumn);
if (existingColumn.type != newColumn.type) {
throw SchemaException(
"May not change column type for '${existingColumn.name}' in '$tableName' (${existingColumn.typeString} -> ${newColumn.typeString})",
);
}
if (existingColumn.autoincrement != newColumn.autoincrement) {
throw SchemaException(
"May not change column autoincrementing behavior for '${existingColumn.name}' in '$tableName'",
);
}
if (existingColumn.isPrimaryKey != newColumn.isPrimaryKey) {
throw SchemaException(
"May not change column primary key status for '${existingColumn.name}' in '$tableName'",
);
}
if (existingColumn.relatedTableName != newColumn.relatedTableName) {
throw SchemaException(
"May not change reference table for foreign key column '${existingColumn.name}' in '$tableName' (${existingColumn.relatedTableName} -> ${newColumn.relatedTableName})",
);
}
if (existingColumn.relatedColumnName != newColumn.relatedColumnName) {
throw SchemaException(
"May not change reference column for foreign key column '${existingColumn.name}' in '$tableName' (${existingColumn.relatedColumnName} -> ${newColumn.relatedColumnName})",
);
}
if (existingColumn.name != newColumn.name) {
renameColumn(tableName, existingColumn.name, newColumn.name);
}
table.replaceColumn(existingColumn, newColumn);
final innerCommands = <String>[];
if (existingColumn.isIndexed != newColumn.isIndexed) {
if (store != null) {
if (newColumn.isIndexed!) {
commands.addAll(store!.addIndexToColumn(table, newColumn));
} else {
commands.addAll(store!.deleteIndexFromColumn(table, newColumn));
}
} else {
innerCommands.add("c.isIndexed = ${newColumn.isIndexed}");
}
}
if (existingColumn.isUnique != newColumn.isUnique) {
if (store != null) {
commands.addAll(store!.alterColumnUniqueness(table, newColumn));
} else {
innerCommands.add('c.isUnique = ${newColumn.isUnique}');
}
}
if (existingColumn.defaultValue != newColumn.defaultValue) {
if (store != null) {
commands.addAll(store!.alterColumnDefaultValue(table, newColumn));
} else {
final value = newColumn.defaultValue == null
? 'null'
: '"${newColumn.defaultValue}"';
innerCommands.add('c.defaultValue = $value');
}
}
if (existingColumn.isNullable != newColumn.isNullable) {
if (store != null) {
commands.addAll(
store!
.alterColumnNullability(table, newColumn, unencodedInitialValue),
);
} else {
innerCommands.add('c.isNullable = ${newColumn.isNullable}');
}
}
if (existingColumn.deleteRule != newColumn.deleteRule) {
if (store != null) {
commands.addAll(store!.alterColumnDeleteRule(table, newColumn));
} else {
innerCommands.add('c.deleteRule = ${newColumn.deleteRule}');
}
}
if (store == null && innerCommands.isNotEmpty) {
commands.add(
'database.alterColumn("$tableName", "$columnName", (c) {${innerCommands.join(";")};});',
);
}
}
/// 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,
}) {
/// 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) {
copy.uniqueColumnSet = null;
}
copy.columns.where((c) => c.isForeignKey).forEach(copy.removeColumn);
changeList?.add("Adding table '${copy.name}'");
createTable(copy);
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,
}) {
for (final c in difference.columnsToAdd) {
changeList?.add(
"Adding column '${c!.name}' to table '${difference.actualTable!.name}'",
);
addColumn(difference.actualTable!.name!, c!);
if (!c.isNullable! && c.defaultValue == null) {
changeList?.add(
"WARNING: This migration may fail if table '${difference.actualTable!.name}' already has rows. "
"Add an 'unencodedInitialValue' to the statement 'database.addColumn(\"${difference.actualTable!.name}\", "
"SchemaColumn(\"${c.name}\", ...)'.");
}
}
for (final c in difference.columnsToRemove) {
changeList?.add(
"Deleting column '${c!.name}' from table '${difference.actualTable!.name}'",
);
deleteColumn(difference.actualTable!.name!, c!.name);
}
for (final columnDiff in difference.columnsToModify) {
changeList?.add(
"Modifying column '${columnDiff.actualColumn!.name}' in '${difference.actualTable!.name}'",
);
alterColumn(difference.actualTable!.name!, columnDiff.actualColumn!.name,
(c) {
c.isIndexed = columnDiff.actualColumn!.isIndexed;
c.defaultValue = columnDiff.actualColumn!.defaultValue;
c.isUnique = columnDiff.actualColumn!.isUnique;
c.isNullable = columnDiff.actualColumn!.isNullable;
c.deleteRule = columnDiff.actualColumn!.deleteRule;
});
if (columnDiff.expectedColumn!.isNullable! &&
!columnDiff.actualColumn!.isNullable! &&
columnDiff.actualColumn!.defaultValue == null) {
changeList?.add(
"WARNING: This migration may fail if table '${difference.actualTable!.name}' already has rows. "
"Add an 'unencodedInitialValue' to the statement 'database.addColumn(\"${difference.actualTable!.name}\", "
"SchemaColumn(\"${columnDiff.actualColumn!.name}\", ...)'.");
}
}
if (difference.uniqueSetDifference?.hasDifferences ?? false) {
changeList?.add(
"Setting unique column constraint of '${difference.actualTable!.name}' to ${difference.uniqueSetDifference!.actualColumnNames}.",
);
alterTable(difference.actualTable!.name!, (t) {
if (difference.uniqueSetDifference!.actualColumnNames.isEmpty) {
t.uniqueColumnSet = null;
} else {
t.uniqueColumnSet = difference.uniqueSetDifference!.actualColumnNames;
}
});
}
}
/// 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}", [');
builder.write(table.columns.map(_getNewColumnExpression).join(","));
builder.write("]");
if (table.uniqueColumnSet != null) {
final set = table.uniqueColumnSet!.map((p) => '"$p"').join(",");
builder.write(", uniqueColumnSetNames: [$set]");
}
builder.write('));');
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) {
builder
.write('SchemaColumn.relationship("${column.name}", ${column.type}');
builder.write(', relatedTableName: "${column.relatedTableName}"');
builder.write(', relatedColumnName: "${column.relatedColumnName}"');
builder.write(", rule: ${column.deleteRule}");
} else {
builder.write('SchemaColumn("${column.name}", ${column.type}');
if (column.isPrimaryKey!) {
builder.write(", isPrimaryKey: true");
} else {
builder.write(", isPrimaryKey: false");
}
if (column.autoincrement!) {
builder.write(", autoincrement: true");
} else {
builder.write(", autoincrement: false");
}
if (column.defaultValue != null) {
builder.write(', defaultValue: "${column.defaultValue}"');
}
if (column.isIndexed!) {
builder.write(", isIndexed: true");
} else {
builder.write(", isIndexed: false");
}
}
if (column.isNullable!) {
builder.write(", isNullable: true");
} else {
builder.write(", isNullable: false");
}
if (column.isUnique!) {
builder.write(", isUnique: true");
} else {
builder.write(", isUnique: false");
}
builder.write(")");
return builder.toString();
}
}

View file

@ -1,669 +0,0 @@
/*
* 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';
/// A portable representation of a database column.
///
/// Instances of this type contain the database-only details of a [ManagedPropertyDescription].
class SchemaColumn {
/// 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, {
this.isIndexed = false,
this.isNullable = false,
this.autoincrement = false,
this.isUnique = false,
this.defaultValue,
this.isPrimaryKey = false,
}) {
_type = typeStringForType(type);
}
/// 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, {
this.isNullable = true,
this.isUnique = false,
this.relatedTableName,
this.relatedColumnName,
DeleteRule rule = DeleteRule.nullify,
}) {
isIndexed = true;
_type = typeStringForType(type);
_deleteRule = deleteRuleStringForDeleteRule(rule);
}
/// 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;
if (desc is ManagedRelationshipDescription) {
isPrimaryKey = false;
relatedTableName = desc.destinationEntity.tableName;
relatedColumnName = desc.destinationEntity.primaryKey;
if (desc.deleteRule != null) {
_deleteRule = deleteRuleStringForDeleteRule(desc.deleteRule!);
}
} else if (desc is ManagedAttributeDescription) {
defaultValue = desc.defaultValue;
isPrimaryKey = desc.isPrimaryKey;
}
_type = typeStringForType(desc.type!.kind);
isNullable = desc.isNullable;
autoincrement = desc.autoincrement;
isUnique = desc.isUnique;
isIndexed = desc.isIndexed;
}
/// 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;
isIndexed = otherColumn.isIndexed;
isNullable = otherColumn.isNullable;
autoincrement = otherColumn.autoincrement;
isUnique = otherColumn.isUnique;
defaultValue = otherColumn.defaultValue;
isPrimaryKey = otherColumn.isPrimaryKey;
relatedTableName = otherColumn.relatedTableName;
relatedColumnName = otherColumn.relatedColumnName;
_deleteRule = otherColumn._deleteRule;
}
/// Creates an instance of [SchemaColumn] from the provided [map].
///
/// Where [map] is typically created by [asMap].
SchemaColumn.fromMap(Map<String, dynamic> map) {
name = map["name"] as String;
_type = map["type"] as String?;
isIndexed = map["indexed"] as bool?;
isNullable = map["nullable"] as bool?;
autoincrement = map["autoincrement"] as bool?;
isUnique = map["unique"] as bool?;
defaultValue = map["defaultValue"] as String?;
isPrimaryKey = map["primaryKey"] as bool?;
relatedTableName = map["relatedTableName"] as String?;
relatedColumnName = map["relatedColumnName"] as String?;
_deleteRule = map["deleteRule"] as String?;
}
/// 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.
late String name;
/// The [SchemaTable] this column belongs to.
///
/// 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.
String? get typeString => _type;
/// The type of this column in a [ManagedDataModel].
ManagedPropertyType? get type => typeFromTypeString(_type);
set type(ManagedPropertyType? t) {
_type = typeStringForType(t);
}
/// Whether or not this column is indexed.
bool? isIndexed = false;
/// Whether or not this column is nullable.
bool? isNullable = false;
/// Whether or not this column is autoincremented.
bool? autoincrement = false;
/// Whether or not this column is unique.
bool? isUnique = false;
/// The default value for this column when inserted into a database.
String? defaultValue;
/// Whether or not this column is the primary key of its [table].
bool? isPrimaryKey = false;
/// The related table name if this column is a foreign key column.
///
/// If this column has a foreign key constraint, this property is the name
/// of the referenced table.
///
/// Null if this column is not a foreign key reference.
String? relatedTableName;
/// The related column if this column is a foreign key column.
///
/// If this column has a foreign key constraint, this property is the name
/// of the reference column in [relatedTableName].
String? relatedColumnName;
/// The delete rule for this column if it is a foreign key column.
///
/// Undefined if not a foreign key column.
DeleteRule? get deleteRule =>
_deleteRule == null ? null : deleteRuleForDeleteRuleString(_deleteRule);
set deleteRule(DeleteRule? t) {
if (t == null) {
_deleteRule = null;
} else {
_deleteRule = deleteRuleStringForDeleteRule(t);
}
}
/// 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;
/// 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 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:
return "integer";
case ManagedPropertyType.doublePrecision:
return "double";
case ManagedPropertyType.bigInteger:
return "bigInteger";
case ManagedPropertyType.boolean:
return "boolean";
case ManagedPropertyType.datetime:
return "datetime";
case ManagedPropertyType.string:
return "string";
case ManagedPropertyType.list:
return null;
case ManagedPropertyType.map:
return null;
case ManagedPropertyType.document:
return "document";
default:
return null;
}
}
/// 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":
return ManagedPropertyType.integer;
case "double":
return ManagedPropertyType.doublePrecision;
case "bigInteger":
return ManagedPropertyType.bigInteger;
case "boolean":
return ManagedPropertyType.boolean;
case "datetime":
return ManagedPropertyType.datetime;
case "string":
return ManagedPropertyType.string;
case "document":
return ManagedPropertyType.document;
default:
return null;
}
}
/// 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:
return "cascade";
case DeleteRule.nullify:
return "nullify";
case DeleteRule.restrict:
return "restrict";
case DeleteRule.setDefault:
return "default";
}
}
/// 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":
return DeleteRule.cascade;
case "nullify":
return DeleteRule.nullify;
case "restrict":
return DeleteRule.restrict;
case "default":
return DeleteRule.setDefault;
}
return null;
}
/// 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,
"type": _type,
"nullable": isNullable,
"autoincrement": autoincrement,
"unique": isUnique,
"defaultValue": defaultValue,
"primaryKey": isPrimaryKey,
"relatedTableName": relatedTableName,
"relatedColumnName": relatedColumnName,
"deleteRule": _deleteRule,
"indexed": isIndexed
};
}
/// 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)";
}
/// The difference between two compared [SchemaColumn]s.
///
/// 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) {
throw SchemaException(
"Cannot change primary key of '${expectedColumn!.table!.name}'",
);
}
if (actualColumn!.relatedColumnName !=
expectedColumn!.relatedColumnName) {
throw SchemaException(
"Cannot change an existing column '${expectedColumn!.table!.name}.${expectedColumn!.name}' to an inverse Relationship",
);
}
if (actualColumn!.relatedTableName != expectedColumn!.relatedTableName) {
throw SchemaException(
"Cannot change type of '${expectedColumn!.table!.name}.${expectedColumn!.name}'",
);
}
if (actualColumn!.type != expectedColumn!.type) {
throw SchemaException(
"Cannot change type of '${expectedColumn!.table!.name}.${expectedColumn!.name}'",
);
}
if (actualColumn!.autoincrement != expectedColumn!.autoincrement) {
throw SchemaException(
"Cannot change autoincrement behavior of '${expectedColumn!.table!.name}.${expectedColumn!.name}'",
);
}
if (expectedColumn!.name.toLowerCase() !=
actualColumn!.name.toLowerCase()) {
_differingProperties.add(
_PropertyDifference(
"name",
expectedColumn!.name,
actualColumn!.name,
),
);
}
if (expectedColumn!.isIndexed != actualColumn!.isIndexed) {
_differingProperties.add(
_PropertyDifference(
"isIndexed",
expectedColumn!.isIndexed,
actualColumn!.isIndexed,
),
);
}
if (expectedColumn!.isUnique != actualColumn!.isUnique) {
_differingProperties.add(
_PropertyDifference(
"isUnique",
expectedColumn!.isUnique,
actualColumn!.isUnique,
),
);
}
if (expectedColumn!.isNullable != actualColumn!.isNullable) {
_differingProperties.add(
_PropertyDifference(
"isNullable",
expectedColumn!.isNullable,
actualColumn!.isNullable,
),
);
}
if (expectedColumn!.defaultValue != actualColumn!.defaultValue) {
_differingProperties.add(
_PropertyDifference(
"defaultValue",
expectedColumn!.defaultValue,
actualColumn!.defaultValue,
),
);
}
if (expectedColumn!.deleteRule != actualColumn!.deleteRule) {
_differingProperties.add(
_PropertyDifference(
"deleteRule",
expectedColumn!.deleteRule,
actualColumn!.deleteRule,
),
);
}
}
}
/// The expected column.
///
/// 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 [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);
/// Provides a human-readable list of differences between the expected and actual database columns.
///
/// Empty is there are no differences.
List<String> get errorMessages {
if (expectedColumn == null && actualColumn != null) {
return [
"Column '${actualColumn!.name}' in table '${actualColumn!.table!.name}' should NOT exist, but is created by migration files"
];
} else if (expectedColumn != null && actualColumn == null) {
return [
"Column '${expectedColumn!.name}' in table '${expectedColumn!.table!.name}' should exist, but is NOT created by migration files"
];
}
return _differingProperties.map((property) {
return property.getErrorMessage(
expectedColumn!.table!.name,
expectedColumn!.name,
);
}).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,360 +0,0 @@
/*
* 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';
import 'package:protevus_database/src/schema/schema.dart';
/// A portable representation of a database table.
///
/// Instances of this type contain the database-only details of a [ManagedEntity]. See also [Schema].
class SchemaTable {
/// Creates an instance of this type with a [name], [columns] and [uniqueColumnSetNames].
SchemaTable(
this.name,
List<SchemaColumn> columns, {
List<String>? uniqueColumnSetNames,
}) {
uniqueColumnSet = uniqueColumnSetNames;
_columns = columns;
}
/// Creates an instance of this type to mirror [entity].
SchemaTable.fromEntity(ManagedEntity entity) {
name = entity.tableName;
final validProperties = entity.properties.values
.where(
(p) =>
(p is ManagedAttributeDescription && !p.isTransient) ||
(p is ManagedRelationshipDescription &&
p.relationshipType == ManagedRelationshipType.belongsTo),
)
.toList();
_columns =
validProperties.map((p) => SchemaColumn.fromProperty(p!)).toList();
uniqueColumnSet = entity.uniquePropertySet?.map((p) => p.name).toList();
}
/// Creates a deep copy of [otherTable].
SchemaTable.from(SchemaTable otherTable) {
name = otherTable.name;
_columns = otherTable.columns.map((col) => SchemaColumn.from(col)).toList();
_uniqueColumnSet = otherTable._uniqueColumnSet;
}
/// Creates an empty table.
SchemaTable.empty();
/// Creates an instance of this type from [map].
///
/// This [map] is typically generated from [asMap];
SchemaTable.fromMap(Map<String, dynamic> map) {
name = map["name"] as String?;
_columns = (map["columns"] as List<Map<String, dynamic>>)
.map((c) => SchemaColumn.fromMap(c))
.toList();
uniqueColumnSet = (map["unique"] as List?)?.cast();
}
/// The [Schema] this table belongs to.
///
/// May be null if not assigned to a [Schema].
Schema? schema;
/// The name of the database table.
String? name;
/// The names of a set of columns that must be unique for each row in this table.
///
/// Are sorted alphabetically. Not modifiable.
List<String>? get uniqueColumnSet =>
_uniqueColumnSet != null ? List.unmodifiable(_uniqueColumnSet!) : null;
set uniqueColumnSet(List<String>? columnNames) {
if (columnNames != null) {
_uniqueColumnSet = List.from(columnNames);
_uniqueColumnSet?.sort((String a, String b) => a.compareTo(b));
} else {
_uniqueColumnSet = null;
}
}
/// An unmodifiable list of [SchemaColumn]s in this table.
List<SchemaColumn> get columns => List.unmodifiable(_columnStorage ?? []);
bool get hasForeignKeyInUniqueSet => columns
.where((c) => c.isForeignKey)
.any((c) => uniqueColumnSet?.contains(c.name) ?? false);
List<SchemaColumn>? _columnStorage;
List<String>? _uniqueColumnSet;
set _columns(List<SchemaColumn> columns) {
_columnStorage = columns;
for (final c in _columnStorage!) {
c.table = this;
}
}
/// Returns a [SchemaColumn] in this instance by its name.
///
/// See [columnForName] for more details.
SchemaColumn? operator [](String columnName) => columnForName(columnName);
/// The differences between two tables.
SchemaTableDifference differenceFrom(SchemaTable table) {
return SchemaTableDifference(this, table);
}
/// Adds [column] to this table.
///
/// Sets [column]'s [SchemaColumn.table] to this instance.
void addColumn(SchemaColumn column) {
if (this[column.name] != null) {
throw SchemaException("Column ${column.name} already exists.");
}
_columnStorage!.add(column);
column.table = this;
}
void renameColumn(SchemaColumn column, String? newName) {
throw SchemaException("Renaming a column not yet implemented!");
// if (!columns.contains(column)) {
// throw new SchemaException("Column ${column.name} does not exist on ${name}.");
// }
//
// if (columnForName(newName) != null) {
// throw new SchemaException("Column ${newName} already exists.");
// }
//
// if (column.isPrimaryKey) {
// throw new SchemaException("May not rename primary key column (${column.name} -> ${newName})");
// }
//
// // We also must rename indices
// column.name = newName;
}
/// Removes [column] from this table.
///
/// Exact [column] must be in this table, else an exception is thrown.
/// Sets [column]'s [SchemaColumn.table] to null.
void removeColumn(SchemaColumn column) {
if (!columns.contains(column)) {
throw SchemaException("Column ${column.name} does not exist on $name.");
}
_columnStorage!.remove(column);
column.table = null;
}
/// Replaces [existingColumn] with [newColumn] in this table.
void replaceColumn(SchemaColumn existingColumn, SchemaColumn newColumn) {
if (!columns.contains(existingColumn)) {
throw SchemaException(
"Column ${existingColumn.name} does not exist on $name.",
);
}
final index = _columnStorage!.indexOf(existingColumn);
_columnStorage![index] = newColumn;
newColumn.table = this;
existingColumn.table = null;
}
/// Returns a [SchemaColumn] with [name].
///
/// Case-insensitively compares names of [columns] with [name]. Returns null if no column exists
/// with [name].
SchemaColumn? columnForName(String name) {
final lowercaseName = name.toLowerCase();
return columns
.firstWhereOrNull((col) => col.name.toLowerCase() == lowercaseName);
}
/// Returns portable representation of this table.
Map<String, dynamic> asMap() {
return {
"name": name,
"columns": columns.map((c) => c.asMap()).toList(),
"unique": uniqueColumnSet
};
}
@override
String toString() => name!;
}
/// The difference between two [SchemaTable]s.
///
/// This class is used for comparing schemas for validation and migration.
class SchemaTableDifference {
/// Creates a new instance that represents the difference between [expectedTable] and [actualTable].
SchemaTableDifference(this.expectedTable, this.actualTable) {
if (expectedTable != null && actualTable != null) {
for (final expectedColumn in expectedTable!.columns) {
final actualColumn =
actualTable != null ? actualTable![expectedColumn.name] : null;
if (actualColumn == null) {
_differingColumns.add(SchemaColumnDifference(expectedColumn, null));
} else {
final diff = expectedColumn.differenceFrom(actualColumn);
if (diff.hasDifferences) {
_differingColumns.add(diff);
}
}
}
_differingColumns.addAll(
actualTable!.columns
.where((t) => expectedTable![t.name] == null)
.map((unexpectedColumn) {
return SchemaColumnDifference(null, unexpectedColumn);
}),
);
uniqueSetDifference =
SchemaTableUniqueSetDifference(expectedTable!, actualTable!);
}
}
/// The expected table.
///
/// May be null if no table is expected.
final SchemaTable? expectedTable;
/// The actual table.
///
/// May be null if there is no table.
final SchemaTable? actualTable;
/// The difference between [SchemaTable.uniqueColumnSet]s.
///
/// Null if either [expectedTable] or [actualTable] are null.
SchemaTableUniqueSetDifference? uniqueSetDifference;
/// Whether or not [expectedTable] and [actualTable] are the same.
bool get hasDifferences =>
_differingColumns.isNotEmpty ||
expectedTable?.name?.toLowerCase() != actualTable?.name?.toLowerCase() ||
(expectedTable == null && actualTable != null) ||
(actualTable == null && expectedTable != null) ||
(uniqueSetDifference?.hasDifferences ?? false);
/// Human-readable list of differences between [expectedTable] and [actualTable].
List<String> get errorMessages {
if (expectedTable == null && actualTable != null) {
return [
"Table '$actualTable' should NOT exist, but is created by migration files."
];
} else if (expectedTable != null && actualTable == null) {
return [
"Table '$expectedTable' should exist, but it is NOT created by migration files."
];
}
final diffs =
_differingColumns.expand((diff) => diff.errorMessages).toList();
diffs.addAll(uniqueSetDifference?.errorMessages ?? []);
return diffs;
}
List<SchemaColumnDifference> get columnDifferences => _differingColumns;
List<SchemaColumn?> get columnsToAdd {
return _differingColumns
.where(
(diff) => diff.expectedColumn == null && diff.actualColumn != null,
)
.map((diff) => diff.actualColumn)
.toList();
}
List<SchemaColumn?> get columnsToRemove {
return _differingColumns
.where(
(diff) => diff.expectedColumn != null && diff.actualColumn == null,
)
.map((diff) => diff.expectedColumn)
.toList();
}
List<SchemaColumnDifference> get columnsToModify {
return _differingColumns
.where(
(columnDiff) =>
columnDiff.expectedColumn != null &&
columnDiff.actualColumn != null,
)
.toList();
}
final List<SchemaColumnDifference> _differingColumns = [];
}
/// Difference between two [SchemaTable.uniqueColumnSet]s.
class SchemaTableUniqueSetDifference {
SchemaTableUniqueSetDifference(
SchemaTable expectedTable,
SchemaTable actualTable,
) : expectedColumnNames = expectedTable.uniqueColumnSet ?? [],
actualColumnNames = actualTable.uniqueColumnSet ?? [],
_tableName = actualTable.name;
/// The expected set of unique column names.
final List<String> expectedColumnNames;
/// The actual set of unique column names.
final List<String> actualColumnNames;
final String? _tableName;
/// Whether or not [expectedColumnNames] and [actualColumnNames] are equivalent.
bool get hasDifferences {
if (expectedColumnNames.length != actualColumnNames.length) {
return true;
}
return !expectedColumnNames.every(actualColumnNames.contains);
}
/// Human-readable list of differences between [expectedColumnNames] and [actualColumnNames].
List<String> get errorMessages {
if (expectedColumnNames.isEmpty && actualColumnNames.isNotEmpty) {
return [
"Multi-column unique constraint on table '$_tableName' "
"should NOT exist, but is created by migration files."
];
} else if (expectedColumnNames.isNotEmpty && actualColumnNames.isEmpty) {
return [
"Multi-column unique constraint on table '$_tableName' "
"should exist, but it is NOT created by migration files."
];
}
if (hasDifferences) {
final expectedColumns = expectedColumnNames.map((c) => "'$c'").join(", ");
final actualColumns = actualColumnNames.map((c) => "'$c'").join(", ");
return [
"Multi-column unique constraint on table '$_tableName' "
"is expected to be for properties $expectedColumns, but is actually $actualColumns"
];
}
return [];
}
}

View file

@ -1,22 +0,0 @@
name: protevus_database
description: The Databse Package for the Protevus Platform
version: 0.0.1
homepage: https://protevus.com
documentation: https://docs.protevus.com
repository: https://git.protevus.com/protevus/platform
environment:
sdk: ^3.4.2
# Add regular dependencies here.
dependencies:
protevus_http: ^0.0.1
protevus_openapi: ^0.0.1
protevus_runtime: ^0.0.1
collection: ^1.18.0
meta: ^1.12.0
# path: ^1.8.0
dev_dependencies:
lints: ^3.0.0
test: ^1.24.0

View file

@ -1,7 +0,0 @@
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/
# Avoid committing pubspec.lock for library packages; see
# https://dart.dev/guides/libraries/private-files#pubspeclock.
pubspec.lock

View file

@ -1,3 +0,0 @@
## 1.0.0
- Initial version.

View file

@ -1,10 +0,0 @@
The MIT License (MIT)
The Laravel Framework is Copyright (c) Taylor Otwell
The Fabric Framework is Copyright (c) Vieo, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1,39 +0,0 @@
<!--
This README describes the package. If you publish this package to pub.dev,
this README's contents appear on the landing page for your package.
For information about how to write a good package README, see the guide for
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
For general information about developing packages, see the Dart guide for
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
and the Flutter guide for
[developing packages and plugins](https://flutter.dev/developing-packages).
-->
TODO: Put a short description of the package here that helps potential users
know whether this package might be useful for them.
## Features
TODO: List what your package can do. Maybe include images, gifs, or videos.
## Getting started
TODO: List prerequisites and provide or point to information on how to
start using the package.
## Usage
TODO: Include short and useful examples for package users. Add longer examples
to `/example` folder.
```dart
const like = 'sample';
```
## Additional information
TODO: Tell users more about the package: where to find more information, how to
contribute to the package, how to file issues, what response they can expect
from the package authors, and more.

View file

@ -1,30 +0,0 @@
# This file configures the static analysis results for your project (errors,
# warnings, and lints).
#
# This enables the 'recommended' set of lints from `package:lints`.
# This set helps identify many issues that may lead to problems when running
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
# style and format.
#
# If you want a smaller set of lints you can change this to specify
# 'package:lints/core.yaml'. These are just the most critical lints
# (the recommended set includes the core lints).
# The core lints are also what is used by pub.dev for scoring packages.
include: package:lints/recommended.yaml
# Uncomment the following section to specify additional rules.
# linter:
# rules:
# - camel_case_types
# analyzer:
# exclude:
# - path/to/excluded/files/**
# For more information about the core and recommended set of lints, see
# https://dart.dev/go/core-lints
# For additional information about configuring this file, see
# https://dart.dev/guides/language/analysis-options

View file

@ -1,20 +0,0 @@
/*
* 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 hashing functionality for the Protevus Platform.
///
/// It exports two main components:
/// - PBKDF2 (Password-Based Key Derivation Function 2) implementation
/// - Salt generation utilities
///
/// These components are essential for secure password hashing and storage.
library;
export 'package:protevus_hashing/src/pbkdf2.dart';
export 'package:protevus_hashing/src/salt.dart';

View file

@ -1,197 +0,0 @@
/*
* 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:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
/// 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 an instance of PBKDF2 capable of generating a key.
///
/// [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;
/// Generates a key from the given password and salt.
///
/// [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 [List<int>] representing the generated key.
///
/// Throws a [PBKDF2Exception] if the derived key would be too long.
List<int> generateKey(
String password,
String salt,
int rounds,
int keyLength,
) {
if (keyLength > (pow(2, 32) - 1) * _blockSize) {
throw PBKDF2Exception("Derived key too long");
}
final numberOfBlocks = (keyLength / _blockSize).ceil();
final hmac = Hmac(hashAlgorithm, utf8.encode(password));
final key = ByteData(keyLength);
var offset = 0;
final saltBytes = utf8.encode(salt);
final saltLength = saltBytes.length;
final inputBuffer = ByteData(saltBytes.length + 4)
..buffer.asUint8List().setRange(0, saltBytes.length, saltBytes);
for (var blockNumber = 1; blockNumber <= numberOfBlocks; blockNumber++) {
inputBuffer.setUint8(saltLength, blockNumber >> 24);
inputBuffer.setUint8(saltLength + 1, blockNumber >> 16);
inputBuffer.setUint8(saltLength + 2, blockNumber >> 8);
inputBuffer.setUint8(saltLength + 3, blockNumber);
final block = _XORDigestSink.generate(inputBuffer, hmac, rounds);
var blockLength = _blockSize;
if (offset + blockLength > keyLength) {
blockLength = keyLength - offset;
}
key.buffer.asUint8List().setRange(offset, offset + blockLength, block);
offset += blockLength;
}
return key.buffer.asUint8List();
}
/// 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,
int rounds,
int keyLength,
) {
const converter = Base64Encoder();
return converter.convert(generateKey(password, salt, rounds, keyLength));
}
}
/// 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);
// If rounds == 1, we have already run the first hash in the constructor
// so this loop won't run.
for (var round = 1; round < rounds; round++) {
final hmacSink = hmac.startChunkedConversion(hashSink);
hmacSink.add(hashSink.lastDigest);
hmacSink.close();
}
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;
for (var i = 0; i < digest.bytes.length; i++) {
bytes.setUint8(i, bytes.getUint8(i) ^ lastDigest[i]);
}
}
/// 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() {}
}

View file

@ -1,34 +0,0 @@
/*
* 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:convert';
import 'dart:math';
import 'dart:typed_data';
/// Generates a random salt of [length] bytes from a cryptographically secure random number generator.
///
/// Each element of this list is a byte.
List<int> generate(int length) {
final buffer = Uint8List(length);
final rng = Random.secure();
for (var i = 0; i < length; i++) {
buffer[i] = rng.nextInt(256);
}
return buffer;
}
/// Generates a random salt of [length] bytes from a cryptographically secure random number generator and encodes it to Base64.
///
/// [length] is the number of bytes generated, not the [length] of the base64 encoded string returned. Decoding
/// the base64 encoded string will yield [length] number of bytes.
String generateAsBase64String(int length) {
const encoder = Base64Encoder();
return encoder.convert(generate(length));
}

View file

@ -1,17 +0,0 @@
name: protevus_hashing
description: The Hashing Package for the Protevus Platform
version: 0.0.1
homepage: https://protevus.com
documentation: https://docs.protevus.com
repository: https://github.com/protevus/platformo
environment:
sdk: ^3.4.2
# Add regular dependencies here.
dependencies:
crypto: ^3.0.3
dev_dependencies:
lints: ^3.0.0
test: ^1.24.0

Some files were not shown because too many files have changed in this diff Show more