From ab78b3d294127ea1ec2c2ee76625b6a1e4af02fd Mon Sep 17 00:00:00 2001 From: Seven Du Date: Wed, 5 Jun 2024 10:36:42 +0800 Subject: [PATCH 01/20] refactor(client): exported the `BinraryEngine` in `orm`, maker `engines/binary.dart` deprecated. --- packages/orm/bin/src/generate_client.dart | 3 +- packages/orm/lib/engines/binary.dart | 292 +----------------- packages/orm/lib/orm.dart | 3 + .../src/runtime/engines/binary_engine.dart | 290 +++++++++++++++++ packages/orm/lib/version.dart | 2 +- 5 files changed, 299 insertions(+), 291 deletions(-) create mode 100644 packages/orm/lib/src/runtime/engines/binary_engine.dart diff --git a/packages/orm/bin/src/generate_client.dart b/packages/orm/bin/src/generate_client.dart index 7506bfb8..8ce3cea6 100644 --- a/packages/orm/bin/src/generate_client.dart +++ b/packages/orm/bin/src/generate_client.dart @@ -137,8 +137,7 @@ extension on Generator { .add(Parameter((p) => p.name = 'defaultDataSources')); final engine = - refer('BinaryEngine', 'package:orm/engines/binary.dart') - .newInstance([], { + refer('BinaryEngine', 'package:orm/orm.dart').newInstance([], { 'schema': refer('schema'), 'datasources': refer('datasourceOverrides'), }); diff --git a/packages/orm/lib/engines/binary.dart b/packages/orm/lib/engines/binary.dart index 2bbbecaa..93c2f2ca 100644 --- a/packages/orm/lib/engines/binary.dart +++ b/packages/orm/lib/engines/binary.dart @@ -1,290 +1,6 @@ +/// TODO: Remove in `v5.x` version. +@Deprecated( + 'Deprecated on `v5.x` version, use `import \'package:orm/orm.dart\'') library prisma.engines.binary; -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; -import 'package:retry/retry.dart'; -import 'package:webfetch/webfetch.dart' show fetch; - -import '../src/runtime/engine.dart'; -import '../src/runtime/json_protocol/deserialize.dart'; -import '../src/runtime/json_protocol/protocol.dart'; -import '../src/runtime/metrics/metrics_format.dart'; -import '../src/runtime/transaction/isolation_level.dart'; -import '../src/runtime/transaction/transaction.dart'; -import '../src/runtime/transaction/transaction_headers.dart'; - -class BinaryEngine extends Engine { - Process? _process; - Uri? _endpoint; - final logger = Logger('prisma.binary-engine'); - - BinaryEngine({required super.schema, required super.datasources}); - - /// Search for prisma binary query engine path. - File get _executable { - final executable = 'prisma-query-engine'; - final searchDirectories = [ - Directory.current.path, - path.join(Directory.current.path, 'prisma'), - path.join(Directory.current.path, '.dart_tool'), - ]; - - for (final directory in searchDirectories) { - final file = File(path.join(directory, executable)); - if (file.existsSync()) { - logger.info('Found query engine binary in ${file.path}'); - return file; - } - } - - logger.severe('No query engine binary found in $searchDirectories'); - throw Exception( - 'No query engine binary found ($executable) in $searchDirectories'); - } - - /// Generate owerwrite datasources. - String get _overwriteDatasources { - final overwrite = datasources.entries - .map((e) => {'name': e.key, 'url': e.value}) - .toList(); - - return base64.encode(utf8.encode(json.encode(overwrite))); - } - - @override - Future start() async { - _process ??= await _internalStart(); - } - - Future _internalStart() async { - if (_process != null) return _process!; - - final arguments = [ - '--enable-metrics', - '--enable-open-telemetry', - '--enable-raw-queries', - ]; - - // Set port. - arguments.addAll(['--port', '0']); - - // Set --datamodel - arguments.addAll(['--datamodel', base64.encode(utf8.encode(schema))]); - - // Set protocol is json - arguments.addAll(['--engine-protocol', 'json']); - - // Set overwrite datasources - arguments.addAll(['--overwrite-datasources', _overwriteDatasources]); - - final process = await Process.start( - path.join( - '.', - path.relative(_executable.path, from: path.current), - ), - arguments, - workingDirectory: path.current, - includeParentEnvironment: true, - environment: { - 'RUST_BACKTRACE': '1', - 'RUST_LOG': 'info', - }, - ); - - _listenProcess(process); - final ready = await _serverReady(); - if (!ready) { - process.kill(); - throw Exception('Unable to start query engine'); - } - - return process; - } - - Future _serverEndpoint() async { - // Await _endpoint is not null - // With `retry` package, retry 5 times with 1 second delay - return retry( - () => _endpoint != null - ? _endpoint! - : throw Exception('Prisma binary query engine not ready'), - ); - } - - /// Server ready - Future _serverReady() async { - // Check if server is ready - final url = (await _serverEndpoint()).resolve('/status'); - - return retry(() async { - final response = await fetch(url); - final data = await response.json(); - - if (data case {'status': 'ok'}) { - return true; - } - - final error = Exception('Prisma binary query engine not ready'); - logger.severe('Prisma binary query engine not ready ($url)', error); - - throw error; - }); - } - - /// Listen process - void _listenProcess(Process process) { - final stream = - process.stdout.transform(utf8.decoder).transform(const LineSplitter()); - stream.listen((message) { - if (message.isEmpty) return; - try { - final data = json.decode(message); - if (data - case { - 'level': "INFO", - 'target': 'query_engine::server', - 'fields': {'ip': final String host, 'port': final String port} - }) { - _endpoint = Uri(scheme: 'http', host: host, port: int.parse(port)); - } - } catch (e, s) { - logger.severe('Unable to parse message: $message', e, s); - rethrow; - } - }); - } - - @override - Future stop() async { - _process?.kill(); - _process = null; - } - - @override - Future request( - JsonQuery query, { - TransactionHeaders? headers, - Transaction? transaction, - }) async { - headers ??= TransactionHeaders(); - - await start(); - final endpoint = await _serverEndpoint(); - - if (transaction != null) { - headers.set('x-transaction-id', transaction.id); - } - - final response = await fetch( - endpoint, - headers: headers, - body: query.toJson(), - method: 'POST', - ); - - final result = await response.json(); - - return switch (result) { - {'data': final Map data} => deserializeJsonResponse(data), - {'errors': final Iterable errors} => throw Exception(errors), - _ => throw Exception(result), - }; - } - - @override - Future commitTransaction({ - required TransactionHeaders headers, - required Transaction transaction, - }) async { - await start(); - - final endpoint = await _serverEndpoint(); - final response = await fetch( - endpoint.resolve('/transaction/${transaction.id}/commit'), - method: 'POST', - headers: headers, - ); - final result = await response.json(); - - return switch (result) { - {'errors': final Iterable errors} => throw Exception(errors), - _ => null, - }; - } - - @override - Future rollbackTransaction({ - required TransactionHeaders headers, - required Transaction transaction, - }) async { - await start(); - - final endpoint = await _serverEndpoint(); - final response = await fetch( - endpoint.resolve('/transaction/${transaction.id}/rollback'), - method: 'POST', - headers: headers, - ); - final result = await response.json(); - - return switch (result) { - {'errors': final Iterable errors} => throw Exception(errors), - _ => null, - }; - } - - @override - Future startTransaction({ - required TransactionHeaders headers, - int maxWait = 2000, - int timeout = 5000, - TransactionIsolationLevel? isolationLevel, - }) async { - await start(); - final endpoint = await _serverEndpoint(); - final response = await fetch( - endpoint.resolve('/transaction/start'), - method: 'POST', - headers: headers, - body: { - 'max_wait': maxWait, - 'timeout': timeout, - if (isolationLevel != null) 'isolation_level': isolationLevel.name, - }, - ); - final result = await response.json(); - - return switch (result) { - {'id': final String id} => Transaction(id), - {'errors': final Iterable errors} => throw Exception(errors), - _ => throw Exception(result), - }; - } - - @override - Future metrics({ - Map? globalLabels, - required MetricsFormat format, - }) async { - await start(); - - final endpoint = await _serverEndpoint(); - final response = await fetch( - endpoint.replace( - path: '/metrics', - queryParameters: {'format': format.name}, - ), - method: 'POST', - body: globalLabels, - ); - - return switch (format) { - MetricsFormat.json => response.json(), - MetricsFormat.prometheus => response.text(), - }; - } -} +export '../src/runtime/engines/binary_engine.dart'; diff --git a/packages/orm/lib/orm.dart b/packages/orm/lib/orm.dart index 8144f098..6b61c9f5 100644 --- a/packages/orm/lib/orm.dart +++ b/packages/orm/lib/orm.dart @@ -27,3 +27,6 @@ export 'src/runtime/metrics/metrics_format.dart'; // Raw export 'src/runtime/raw/raw_client.dart'; + +// Engines +export 'src/runtime/engines/binary_engine.dart'; diff --git a/packages/orm/lib/src/runtime/engines/binary_engine.dart b/packages/orm/lib/src/runtime/engines/binary_engine.dart new file mode 100644 index 00000000..9a49614f --- /dev/null +++ b/packages/orm/lib/src/runtime/engines/binary_engine.dart @@ -0,0 +1,290 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; +import 'package:retry/retry.dart'; +import 'package:webfetch/webfetch.dart' show fetch; + +import '../engine.dart'; +import '../json_protocol/deserialize.dart'; +import '../json_protocol/protocol.dart'; +import '../metrics/metrics_format.dart'; +import '../transaction/isolation_level.dart'; +import '../transaction/transaction.dart'; +import '../transaction/transaction_headers.dart'; + +/// Prisma branary engine. +class BinaryEngine extends Engine { + Process? _process; + Uri? _endpoint; + final logger = Logger('prisma.binary-engine'); + + /// Creates a new [BinaryEngine]. + BinaryEngine({required super.schema, required super.datasources}); + + /// Search for prisma binary query engine path. + File get _executable { + final executable = 'prisma-query-engine'; + final searchDirectories = [ + Directory.current.path, + path.join(Directory.current.path, 'prisma'), + path.join(Directory.current.path, '.dart_tool'), + ]; + + for (final directory in searchDirectories) { + final file = File(path.join(directory, executable)); + if (file.existsSync()) { + logger.info('Found query engine binary in ${file.path}'); + return file; + } + } + + logger.severe('No query engine binary found in $searchDirectories'); + throw Exception( + 'No query engine binary found ($executable) in $searchDirectories'); + } + + /// Generate owerwrite datasources. + String get _overwriteDatasources { + final overwrite = datasources.entries + .map((e) => {'name': e.key, 'url': e.value}) + .toList(); + + return base64.encode(utf8.encode(json.encode(overwrite))); + } + + @override + Future start() async { + _process ??= await _internalStart(); + } + + Future _internalStart() async { + if (_process != null) return _process!; + + final arguments = [ + '--enable-metrics', + '--enable-open-telemetry', + '--enable-raw-queries', + ]; + + // Set port. + arguments.addAll(['--port', '0']); + + // Set --datamodel + arguments.addAll(['--datamodel', base64.encode(utf8.encode(schema))]); + + // Set protocol is json + arguments.addAll(['--engine-protocol', 'json']); + + // Set overwrite datasources + arguments.addAll(['--overwrite-datasources', _overwriteDatasources]); + + final process = await Process.start( + path.join( + '.', + path.relative(_executable.path, from: path.current), + ), + arguments, + workingDirectory: path.current, + includeParentEnvironment: true, + environment: { + 'RUST_BACKTRACE': '1', + 'RUST_LOG': 'info', + }, + ); + + _listenProcess(process); + final ready = await _serverReady(); + if (!ready) { + process.kill(); + throw Exception('Unable to start query engine'); + } + + return process; + } + + Future _serverEndpoint() async { + // Await _endpoint is not null + // With `retry` package, retry 5 times with 1 second delay + return retry( + () => _endpoint != null + ? _endpoint! + : throw Exception('Prisma binary query engine not ready'), + ); + } + + /// Server ready + Future _serverReady() async { + // Check if server is ready + final url = (await _serverEndpoint()).resolve('/status'); + + return retry(() async { + final response = await fetch(url); + final data = await response.json(); + + if (data case {'status': 'ok'}) { + return true; + } + + final error = Exception('Prisma binary query engine not ready'); + logger.severe('Prisma binary query engine not ready ($url)', error); + + throw error; + }); + } + + /// Listen process + void _listenProcess(Process process) { + final stream = + process.stdout.transform(utf8.decoder).transform(const LineSplitter()); + stream.listen((message) { + if (message.isEmpty) return; + try { + final data = json.decode(message); + if (data + case { + 'level': "INFO", + 'target': 'query_engine::server', + 'fields': {'ip': final String host, 'port': final String port} + }) { + _endpoint = Uri(scheme: 'http', host: host, port: int.parse(port)); + } + } catch (e, s) { + logger.severe('Unable to parse message: $message', e, s); + rethrow; + } + }); + } + + @override + Future stop() async { + _process?.kill(); + _process = null; + } + + @override + Future request( + JsonQuery query, { + TransactionHeaders? headers, + Transaction? transaction, + }) async { + headers ??= TransactionHeaders(); + + await start(); + final endpoint = await _serverEndpoint(); + + if (transaction != null) { + headers.set('x-transaction-id', transaction.id); + } + + final response = await fetch( + endpoint, + headers: headers, + body: query.toJson(), + method: 'POST', + ); + + final result = await response.json(); + + return switch (result) { + {'data': final Map data} => deserializeJsonResponse(data), + {'errors': final Iterable errors} => throw Exception(errors), + _ => throw Exception(result), + }; + } + + @override + Future commitTransaction({ + required TransactionHeaders headers, + required Transaction transaction, + }) async { + await start(); + + final endpoint = await _serverEndpoint(); + final response = await fetch( + endpoint.resolve('/transaction/${transaction.id}/commit'), + method: 'POST', + headers: headers, + ); + final result = await response.json(); + + return switch (result) { + {'errors': final Iterable errors} => throw Exception(errors), + _ => null, + }; + } + + @override + Future rollbackTransaction({ + required TransactionHeaders headers, + required Transaction transaction, + }) async { + await start(); + + final endpoint = await _serverEndpoint(); + final response = await fetch( + endpoint.resolve('/transaction/${transaction.id}/rollback'), + method: 'POST', + headers: headers, + ); + final result = await response.json(); + + return switch (result) { + {'errors': final Iterable errors} => throw Exception(errors), + _ => null, + }; + } + + @override + Future startTransaction({ + required TransactionHeaders headers, + int maxWait = 2000, + int timeout = 5000, + TransactionIsolationLevel? isolationLevel, + }) async { + await start(); + final endpoint = await _serverEndpoint(); + final response = await fetch( + endpoint.resolve('/transaction/start'), + method: 'POST', + headers: headers, + body: { + 'max_wait': maxWait, + 'timeout': timeout, + if (isolationLevel != null) 'isolation_level': isolationLevel.name, + }, + ); + final result = await response.json(); + + return switch (result) { + {'id': final String id} => Transaction(id), + {'errors': final Iterable errors} => throw Exception(errors), + _ => throw Exception(result), + }; + } + + @override + Future metrics({ + Map? globalLabels, + required MetricsFormat format, + }) async { + await start(); + + final endpoint = await _serverEndpoint(); + final response = await fetch( + endpoint.replace( + path: '/metrics', + queryParameters: {'format': format.name}, + ), + method: 'POST', + body: globalLabels, + ); + + return switch (format) { + MetricsFormat.json => response.json(), + MetricsFormat.prometheus => response.text(), + }; + } +} diff --git a/packages/orm/lib/version.dart b/packages/orm/lib/version.dart index c13ac401..58d44db5 100644 --- a/packages/orm/lib/version.dart +++ b/packages/orm/lib/version.dart @@ -1,3 +1,3 @@ library prisma.version; -const version = '4.1.0'; +const version = '4.4.0'; From 62d935e0fa563dc13b9336ffdc4ee3ec328f43a8 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Wed, 5 Jun 2024 10:54:18 +0800 Subject: [PATCH 02/20] feat: Add error classes --- packages/orm/lib/orm.dart | 1 + packages/orm/lib/src/errors.dart | 73 ++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 packages/orm/lib/src/errors.dart diff --git a/packages/orm/lib/orm.dart b/packages/orm/lib/orm.dart index 6b61c9f5..1c6cf3ae 100644 --- a/packages/orm/lib/orm.dart +++ b/packages/orm/lib/orm.dart @@ -1,5 +1,6 @@ library prisma.runtime; +export 'src/errors.dart'; export 'src/runtime/action_client.dart'; export 'src/runtime/decimal.dart'; export 'src/runtime/engine.dart'; diff --git a/packages/orm/lib/src/errors.dart b/packages/orm/lib/src/errors.dart new file mode 100644 index 00000000..9c00ad23 --- /dev/null +++ b/packages/orm/lib/src/errors.dart @@ -0,0 +1,73 @@ +import '../../version.dart'; + +/// Prisma client abstract error. +abstract class PrismaClientError extends Error { + /// Error message associated with [error code](https://www.prisma.io/docs/orm/reference/error-reference#error-codes) + final String message; + + /// Version of Prisma client (for example, `4.4.0`) + final String clientVersion; + + /// Creates a new Prisma client error. + PrismaClientError({required this.message, this.clientVersion = version}); +} + +/// Prisma Client throws a [PrismaClientValidationError] exception if validation fails - for example: +/// +/// * Missing field - for example, an empty `data: {}` property when creating a new record +/// * Incorrect field type provided (for example, setting a [bool] field to `"Hello, I like cheese and gold!"`) +class PrismaClientValidationError extends PrismaClientError { + /// Create s a new [PrismaClientValidationError]. + PrismaClientValidationError({required super.message, super.clientVersion}); +} + +/// Prisma Client throws a [PrismaClientUnknownRequestError] exception if the query engine returns an error related to a request that does not have an error code. +class PrismaClientUnknownRequestError extends PrismaClientError { + PrismaClientUnknownRequestError( + {required super.message, super.clientVersion}); +} + +/// Prisma Client throws a [PrismaClientRustPanicError] exception if the underlying engine crashes and exits with a non-zero exit code. In this case, Prisma Client or the whole Node process must be restarted. +class PrismaClientRustPanicError extends PrismaClientError { + PrismaClientRustPanicError({required super.message, super.clientVersion}); +} + +/// Prisma Client throws a [PrismaClientInitializationError] exception if something goes wrong when the query engine is started and the connection to the database is created. This happens either: +/// +/// * When `prisma.$connect()`` is called OR +/// * When the first query is executed +/// +/// Errors that can occur include: +/// +/// * The provided credentials for the database are invalid +/// * There is no database server running under the provided hostname and port +/// * The port that the query engine HTTP server wants to bind to is already taken +/// * missing or inaccessible environment variable +/// * The query engine binary for the current platform could not be found (`generator` block) +class PrismaClientInitializationError extends PrismaClientError { + /// A Prisma-specific error code. + final String errorCode; + + PrismaClientInitializationError( + {required this.errorCode, required super.message, super.clientVersion}); +} + +/// Prisma Client throws a [PrismaClientKnownRequestError] exception if the query engine returns a known error related to the request - for example, a unique constraint violation. +class PrismaClientKnownRequestError extends PrismaClientError { + /// A Prisma-specific [error code](https://www.prisma.io/docs/orm/reference/error-reference#error-codes). + final String code; + + /// Additional information about the error + /// for example, the field that caused the error: + /// + /// ```json + /// { target: [ 'email' ] } + /// ``` + final Object? meta; + + PrismaClientKnownRequestError( + {required this.code, + required super.message, + super.clientVersion, + this.meta}); +} From 442dc9dc300613e0bb8825d7048764e85b269f1e Mon Sep 17 00:00:00 2001 From: Seven Du Date: Wed, 5 Jun 2024 15:31:09 +0800 Subject: [PATCH 03/20] feat(client): add logging --- packages/orm/lib/src/logging.dart | 94 +++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 packages/orm/lib/src/logging.dart diff --git a/packages/orm/lib/src/logging.dart b/packages/orm/lib/src/logging.dart new file mode 100644 index 00000000..2783e4a1 --- /dev/null +++ b/packages/orm/lib/src/logging.dart @@ -0,0 +1,94 @@ +/// Log level +enum LogLevel { info, query, warn, error } + +/// Log emit +enum LogEmit { stdout, event } + +/// Prisma engine log event. +/// +/// @see [QueryEvent] +/// @see [LogEvent] +base class EngineEvent { + /// Event timestamp. + final DateTime timestamp; + + /// Event target. + final String target; + + const EngineEvent({required this.timestamp, required this.target}); +} + +/// Log event. +final class LogEvent extends EngineEvent { + /// Log message. + final String message; + + const LogEvent( + {required super.timestamp, required super.target, required this.message}); +} + +/// Query log event. +final class QueryEvent extends EngineEvent { + final String query; + final String params; + final double duration; + + const QueryEvent( + {required super.timestamp, + required super.target, + required this.duration, + required this.params, + required this.query}); +} + +typedef LogDefinition = Set<(LogLevel level, LogEmit emit)>; +typedef LogListener = void Function(EngineEvent event); + +/// Prisma log event emitter. +class LogEmitter { + final LogDefinition _definition; + final _listeners = <(LogLevel, LogListener)>[]; + + LogEmitter(LogDefinition definition) : _definition = definition; + + void on(LogLevel level, LogListener listener) { + final extsis = _listeners.any((e) => e.$1 == level && e.$2 == listener); + if (extsis) return; + + _listeners.add((level, listener)); + } + + void emit(LogLevel level, EngineEvent event) { + _emitStdout(level, event); + _emitEvent(level, event); + } + + void _emitStdout(LogLevel level, EngineEvent event) { + if (_definition.should(level, LogEmit.stdout)) { + final name = 'prisma:${level.name}'; + final message = switch (event) { + LogEvent(message: final String message) => message, + QueryEvent(query: final String query, params: final String params) => + '$query - $params', + _ => 'Unknown log event: $event', + }; + + print('$name $message'); + } + } + + void _emitEvent(LogLevel level, EngineEvent event) { + if (!_definition.should(level, LogEmit.event)) return; + + final listeners = _listeners.where((e) => e.$1 == level).map((e) => e.$2); + for (final listener in listeners) { + listener(event); + } + } +} + +extension on LogDefinition { + bool should(LogLevel level, LogEmit emit) { + return any((e) => e.$1 == level && e.$2 == emit); + } +} From fa5f97f69984572df8f8c56d5a3a18547b38757c Mon Sep 17 00:00:00 2001 From: Seven Du Date: Wed, 5 Jun 2024 15:31:21 +0800 Subject: [PATCH 04/20] chore: change meta type --- packages/orm/lib/src/errors.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/orm/lib/src/errors.dart b/packages/orm/lib/src/errors.dart index 9c00ad23..9b38647e 100644 --- a/packages/orm/lib/src/errors.dart +++ b/packages/orm/lib/src/errors.dart @@ -63,7 +63,7 @@ class PrismaClientKnownRequestError extends PrismaClientError { /// ```json /// { target: [ 'email' ] } /// ``` - final Object? meta; + final Map? meta; PrismaClientKnownRequestError( {required this.code, From 03fe962921c8b963d9016f3a79c4978f91bb35d7 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Wed, 5 Jun 2024 15:31:42 +0800 Subject: [PATCH 05/20] feat(client): add `env` support --- .../lib/src/env/_create_environment.io.dart | 40 +++++++++++++++ packages/orm/lib/src/env/_env.shared.dart | 21 ++++++++ packages/orm/lib/src/env/env.dart | 50 +++++++++++++++++++ packages/orm/pubspec.yaml | 1 + 4 files changed, 112 insertions(+) create mode 100644 packages/orm/lib/src/env/_create_environment.io.dart create mode 100644 packages/orm/lib/src/env/_env.shared.dart create mode 100644 packages/orm/lib/src/env/env.dart diff --git a/packages/orm/lib/src/env/_create_environment.io.dart b/packages/orm/lib/src/env/_create_environment.io.dart new file mode 100644 index 00000000..a5d53256 --- /dev/null +++ b/packages/orm/lib/src/env/_create_environment.io.dart @@ -0,0 +1,40 @@ +import 'dart:io'; + +import 'package:path/path.dart' as path; +import 'package:dotenv/dotenv.dart' show DotEnv; + +import '_env.shared.dart' as shared; + +class _IOEnvironmentImpl implements shared.Environment { + final shared.Environment env; + final DotEnv dotenv; + + const _IOEnvironmentImpl(this.env, this.dotenv); + + @override + void add(Map other) => env.add(other); + + @override + String? call(String name) => env(name) ?? dotenv[name]; +} + +shared.Environment createEnvironment() { + final env = shared.createEnvironment(); + env.add(Platform.environment); + + final dotenv = DotEnv(includePlatformEnvironment: false, quiet: true); + dotenv.load(_envFiles()); + + return _IOEnvironmentImpl(env, dotenv); +} + +Iterable _envFiles() sync* { + final dirs = [ + Directory.current.path, + path.dirname(Platform.script.toFilePath(windows: Platform.isWindows)), + ]; + for (final dir in dirs) { + yield path.join(dir, '.env'); + yield path.join(dir, 'prisma', '.env'); + } +} diff --git a/packages/orm/lib/src/env/_env.shared.dart b/packages/orm/lib/src/env/_env.shared.dart new file mode 100644 index 00000000..c285225f --- /dev/null +++ b/packages/orm/lib/src/env/_env.shared.dart @@ -0,0 +1,21 @@ +/// Prisma environment. +class Environment { + Environment._(); + + final _enviroment = {}; + + /// Returns a enviroment value by [name], if the [name] + /// not contains, return [null]. + String? call(String name) { + if (bool.hasEnvironment(name)) return String.fromEnvironment(name); + if (_enviroment.containsKey(name)) return _enviroment[name]; + + return null; + } + + /// Add [other] to the enviroment. + void add(Map other) => _enviroment.addAll(other); +} + +/// Internal, create enviroment. +Environment createEnvironment() => Environment._(); diff --git a/packages/orm/lib/src/env/env.dart b/packages/orm/lib/src/env/env.dart new file mode 100644 index 00000000..d6934d6a --- /dev/null +++ b/packages/orm/lib/src/env/env.dart @@ -0,0 +1,50 @@ +import '_env.shared.dart' show Environment; +import '_env.shared.dart' if (dart.library.io) '_create_environment.io.dart' + show createEnvironment; + +export '_env.shared.dart' show Environment; + +/// Enviroment +/// +/// Match stack: +/// 1. `bool.hasEnviroment`, Using the `--define` key-value part. +/// 2. Internal enviroment map, contains key, if extsis return it. +/// 3. load `.env` from `/.env` or `/prisma.env` file. +/// +/// Example: +/// ```dart +/// // Read a env value. +/// final debug = env('DEBUG'); // String or null +/// +/// // Add other env +/// env.add({'HI': "Hello"}); +/// final hi = env('HI'); // "Hello" +/// ``` +final env = createEnvironment(); + +extension EnvironmentUtils on Environment { + /// Returns a value parsed to [bool] type. + /// + /// If the value is `true` | `on` | `1` return `true`, other reurn `false` value. + bool boolean(String name) { + const trueStrings = ['true', '1', 'on']; + + return trueStrings.contains(this(name)); + } + + /// Returns a value parsed to [num] type. + /// + /// Try parse string value, if faild return `0` + num number(String name) { + final value = this(name); + if (value == null) return 0; + + final number = num.tryParse(value); + if (number != null) return number; + + final bigint = BigInt.tryParse(value); + if (bigint != null) bigint.toDouble(); + + return 0; + } +} diff --git a/packages/orm/pubspec.yaml b/packages/orm/pubspec.yaml index 16f9da52..81bf8734 100644 --- a/packages/orm/pubspec.yaml +++ b/packages/orm/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: code_builder: ^4.8.0 dart_style: ^2.3.4 decimal: ^3.0.0 + dotenv: ^4.2.0 json_rpc_2: ^3.0.2 logging: ^1.2.0 path: ^1.8.3 From 957372a762c2ecd2806d72945978fa94883153b7 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Wed, 5 Jun 2024 15:32:25 +0800 Subject: [PATCH 06/20] refactor(client): add base client and client options --- packages/orm/lib/src/base_prisma_client.dart | 26 +++++++++++ .../orm/lib/src/prisma_client_options.dart | 43 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 packages/orm/lib/src/base_prisma_client.dart create mode 100644 packages/orm/lib/src/prisma_client_options.dart diff --git a/packages/orm/lib/src/base_prisma_client.dart b/packages/orm/lib/src/base_prisma_client.dart new file mode 100644 index 00000000..0ea6cfe1 --- /dev/null +++ b/packages/orm/lib/src/base_prisma_client.dart @@ -0,0 +1,26 @@ +import 'logging.dart'; +import 'prisma_client_options.dart'; +import 'runtime/engine.dart'; + +/// Base prisma client +abstract class BasePrismaClient { + BasePrismaClient({ + String? datasourceUrl, + final Map? datasources, + ErrorFormat errorFormat = ErrorFormat.colorless, + LogDefinition? log, + }) : _options = PrismaClientOptions( + datasources: datasources, + datasourceUrl: datasourceUrl, + errorFormat: errorFormat, + logEmitter: LogEmitter(log ?? const {}), + ); + + final PrismaClientOptions _options; + + /// Prisma client options + PrismaClientOptions get $options => _options; + + /// Returns the [Engine] instance typeof [E]. + E get $engine; +} diff --git a/packages/orm/lib/src/prisma_client_options.dart b/packages/orm/lib/src/prisma_client_options.dart new file mode 100644 index 00000000..a1f1fa63 --- /dev/null +++ b/packages/orm/lib/src/prisma_client_options.dart @@ -0,0 +1,43 @@ +import 'logging.dart'; + +/// Error format +enum ErrorFormat { + ///Enables pretty error formatting. + pretty, + + ///Enables colorless error formatting. + colorless, + + /// Enables minimal error formatting. + minimal +} + +class PrismaClientOptions { + /// Overwrites the primary datasource url from your schema.prisma file + final String? datasourceUrl; + + /// Overwrites the datasource url from your schema.prisma file + /// + /// Example: + /// ```dart + /// final prisma = PrismaClient( + /// datasources: {'db': 'file:./dev.db'} + /// ); + /// ``` + final Map? datasources; + + /// Determines the level and formatting of errors returned by Prisma Client. + final ErrorFormat errorFormat; + + /// Log emitter. + final LogEmitter logEmitter; + + /// Create Prisma client options + const PrismaClientOptions({ + this.datasourceUrl, + this.datasources, + required this.errorFormat, + required this.logEmitter, + }) : assert(!(datasourceUrl != null && datasources != null), + 'DatasourceUrl and datasources cannot be used together'); +} From b51a5d81dc1c47babda7800b5282d37f8b747197 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Wed, 5 Jun 2024 16:03:00 +0800 Subject: [PATCH 07/20] chore: WIP, TODO: remove `dotenv` package Q: Why need remove `dotenv`? A: The implementation is too rough and does not expose the env map object, which does not meet the requirements. --- packages/orm/lib/src/env/env.dart | 18 ++++++++++++++++++ packages/orm/lib/src/runtime/engine.dart | 18 +++++++++++++++++- .../lib/src/runtime/engines/binary_engine.dart | 7 ++++++- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/orm/lib/src/env/env.dart b/packages/orm/lib/src/env/env.dart index d6934d6a..916bd7fd 100644 --- a/packages/orm/lib/src/env/env.dart +++ b/packages/orm/lib/src/env/env.dart @@ -47,4 +47,22 @@ extension EnvironmentUtils on Environment { return 0; } + + /// Configure the Prisma client and engine. + /// + /// - [debug]: `DEBUG` is used to enable debugging output in Prisma Client. @see https://www.prisma.io/docs/orm/prisma-client/debugging-and-troubleshooting/debugging + /// - [noColor]: @see https://www.prisma.io/docs/orm/reference/environment-variables-reference#no_color + /// - [queryEngineBinary]: Custom binary query engine location, Only used by generators and binary engine. See https://www.prisma.io/docs/orm/reference/environment-variables-reference#prisma_query_engine_binary + void configure(String? debug, bool? noColor, String? queryEngineBinary) { + final configure = { + if (debug != null) 'DEBUG': debug, + if (noColor != null) 'NO_COLOR': noColor ? 'true' : 'false', + if (queryEngineBinary != null) + 'PRISMA_QUERY_ENGINE_BINARY': queryEngineBinary, + }; + + if (configure.isNotEmpty) { + add(configure); + } + } } diff --git a/packages/orm/lib/src/runtime/engine.dart b/packages/orm/lib/src/runtime/engine.dart index 487003d0..3124d906 100644 --- a/packages/orm/lib/src/runtime/engine.dart +++ b/packages/orm/lib/src/runtime/engine.dart @@ -1,18 +1,34 @@ import 'dart:async'; +import '../prisma_client_options.dart'; import 'json_protocol/protocol.dart'; import 'metrics/metrics_format.dart'; import 'transaction/isolation_level.dart'; import 'transaction/transaction_headers.dart'; import 'transaction/transaction.dart'; +enum DatasourceType { url, enviroment } + +class Datasource { + final DatasourceType type; + final String value; + + const Datasource(this.type, this.value); +} + +typedef Datasources = Map; + abstract class Engine { final String schema; - final Map datasources; + final Datasources datasources; + final Datasources? overwriteDatasources; + final PrismaClientOptions options; const Engine({ + required this.options, required this.schema, required this.datasources, + this.overwriteDatasources, }); /// Starts the engine. diff --git a/packages/orm/lib/src/runtime/engines/binary_engine.dart b/packages/orm/lib/src/runtime/engines/binary_engine.dart index 9a49614f..30e45e1c 100644 --- a/packages/orm/lib/src/runtime/engines/binary_engine.dart +++ b/packages/orm/lib/src/runtime/engines/binary_engine.dart @@ -22,7 +22,12 @@ class BinaryEngine extends Engine { final logger = Logger('prisma.binary-engine'); /// Creates a new [BinaryEngine]. - BinaryEngine({required super.schema, required super.datasources}); + BinaryEngine({ + required super.schema, + required super.datasources, + required super.options, + super.overwriteDatasources, + }); /// Search for prisma binary query engine path. File get _executable { From fcf8a5e766852c74832a446817d0b8c73cfd0219 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Thu, 6 Jun 2024 04:17:34 +0800 Subject: [PATCH 08/20] chore: remove dotenv using `rc` package --- packages/orm/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/orm/pubspec.yaml b/packages/orm/pubspec.yaml index 81bf8734..e4261fb6 100644 --- a/packages/orm/pubspec.yaml +++ b/packages/orm/pubspec.yaml @@ -17,10 +17,10 @@ dependencies: code_builder: ^4.8.0 dart_style: ^2.3.4 decimal: ^3.0.0 - dotenv: ^4.2.0 json_rpc_2: ^3.0.2 logging: ^1.2.0 path: ^1.8.3 + rc: ^0.3.0 recase: ^4.1.0 retry: ^3.1.2 stream_channel: ^2.1.2 From 93b250a5821c9b04cce79e4b044f999d6da61cdc Mon Sep 17 00:00:00 2001 From: Seven Du Date: Thu, 6 Jun 2024 21:03:13 +0800 Subject: [PATCH 09/20] refactor(engine): Refactoring binary engines --- packages/orm/bin/orm.dart | 20 +- packages/orm/bin/src/generate_client.dart | 3 +- packages/orm/bin/src/generator+client.dart | 77 +++ packages/orm/lib/engines/binary.dart | 5 +- packages/orm/lib/orm.dart | 13 +- .../lib/src/_internal/project_directory.dart | 22 +- packages/orm/lib/src/base_prisma_client.dart | 3 + packages/orm/lib/src/config/_loader.dart | 3 + packages/orm/lib/src/config/_loader.io.dart | 9 + .../orm/lib/src/config/prisma+config.dart | 18 + packages/orm/lib/src/config/prisma+env.dart | 26 ++ .../datasources/_validate_datasource_url.dart | 33 ++ .../_validate_datasource_url.io.dart | 23 + .../orm/lib/src/datasources/datasources.dart | 10 + .../datasources/prisma+datasource_utils.dart | 12 + .../orm/lib/src/engines/binary_engine.dart | 439 ++++++++++++++++++ .../lib/src/env/_create_environment.io.dart | 40 -- packages/orm/lib/src/env/_env.shared.dart | 21 - packages/orm/lib/src/env/env.dart | 68 --- packages/orm/lib/src/errors.dart | 13 +- packages/orm/lib/src/logging.dart | 11 +- packages/orm/lib/src/prisma_namespace.dart | 8 + packages/orm/lib/src/runtime/engine.dart | 15 +- .../src/runtime/engines/binary_engine.dart | 295 ------------ .../orm/lib/src/runtime/raw/raw_client.dart | 3 +- .../transaction/transaction_client.dart | 3 +- packages/orm/pubspec.yaml | 2 +- 27 files changed, 727 insertions(+), 468 deletions(-) create mode 100644 packages/orm/bin/src/generator+client.dart create mode 100644 packages/orm/lib/src/config/_loader.dart create mode 100644 packages/orm/lib/src/config/_loader.io.dart create mode 100644 packages/orm/lib/src/config/prisma+config.dart create mode 100644 packages/orm/lib/src/config/prisma+env.dart create mode 100644 packages/orm/lib/src/datasources/_validate_datasource_url.dart create mode 100644 packages/orm/lib/src/datasources/_validate_datasource_url.io.dart create mode 100644 packages/orm/lib/src/datasources/datasources.dart create mode 100644 packages/orm/lib/src/datasources/prisma+datasource_utils.dart create mode 100644 packages/orm/lib/src/engines/binary_engine.dart delete mode 100644 packages/orm/lib/src/env/_create_environment.io.dart delete mode 100644 packages/orm/lib/src/env/_env.shared.dart delete mode 100644 packages/orm/lib/src/env/env.dart create mode 100644 packages/orm/lib/src/prisma_namespace.dart delete mode 100644 packages/orm/lib/src/runtime/engines/binary_engine.dart diff --git a/packages/orm/bin/orm.dart b/packages/orm/bin/orm.dart index e1fd7745..0366b7f7 100644 --- a/packages/orm/bin/orm.dart +++ b/packages/orm/bin/orm.dart @@ -8,6 +8,7 @@ import 'package:path/path.dart'; import 'src/generator.dart'; import 'src/utils/is_flutter_engine_type.dart'; +import 'src/generator+client.dart'; void main() async { final app = GeneratorApp.stdio(stdin: stdin, stdout: stderr); @@ -53,11 +54,20 @@ Future generate(GeneratorOptions options) async { await output.writeAsString(formated); } - // Copy prisma query engine. - final engineDownloadPath = - options.binaryPaths.queryEngine?.values.firstOrNull; - if (engineDownloadPath != null) { - await File(engineDownloadPath).copy('prisma-query-engine'); + final client = formatter.format(generator.client()); + final file = + await File(join(options.generator.output!.value, 'new_client.dart')) + .autoCreate(); + + await file.writeAsString(client); + + if (isFlutterEngineType(options.generator.config)) { + // Copy prisma query engine. + final engineDownloadPath = + options.binaryPaths.queryEngine?.values.firstOrNull; + if (engineDownloadPath != null) { + await File(engineDownloadPath).copy('prisma-query-engine'); + } } } diff --git a/packages/orm/bin/src/generate_client.dart b/packages/orm/bin/src/generate_client.dart index 8ce3cea6..7506bfb8 100644 --- a/packages/orm/bin/src/generate_client.dart +++ b/packages/orm/bin/src/generate_client.dart @@ -137,7 +137,8 @@ extension on Generator { .add(Parameter((p) => p.name = 'defaultDataSources')); final engine = - refer('BinaryEngine', 'package:orm/orm.dart').newInstance([], { + refer('BinaryEngine', 'package:orm/engines/binary.dart') + .newInstance([], { 'schema': refer('schema'), 'datasources': refer('datasourceOverrides'), }); diff --git a/packages/orm/bin/src/generator+client.dart b/packages/orm/bin/src/generator+client.dart new file mode 100644 index 00000000..53d6f018 --- /dev/null +++ b/packages/orm/bin/src/generator+client.dart @@ -0,0 +1,77 @@ +// ignore_for_file: file_names + +import 'generator.dart'; +import 'package:orm/generator_helper.dart' as gh; + +import 'utils/is_flutter_engine_type.dart'; + +extension Generate$Client on Generator { + String client() { + return ''' + import 'package:orm/orm.dart'; + import '$enginePackageUrl' show $engineName; + + class PrismaClient extends BasePrismaClient { + Engine? _engine; + + PrismaClient({ + super.datasourceUrl, + super.datasources, + super.errorFormat, + super.log, + Engine? engine, + }) : _engine = engine; + + @override + Engine get \$engine => _engine ??= createEngineInstance(); + } + + extension on PrismaClient { + Engine createEngineInstance() { + return $engineName( + schema: '${options.schema.literal()}', + datasources: $datasources, + options: \$options, + ); + } + } + '''; + } + + String get datasources { + final datasources = options.datasources.map((e) { + final datasource = switch (e.url) { + gh.EnvVar(name: final String name) => + 'const Datasource(DatasourceType.enviroment, \'${name.literal()}\')', + gh.EnvValue(value: final String url) => + 'const Datasource(DatasourceType.url, \'${url.literal()}\')', + }; + + return '\'${e.name}\': $datasource'; + }); + + return '{${datasources.join(',')}}'; + } + + String get enginePackageUrl { + return switch (isFlutterEngineType(options.generator.config)) { + true => 'package:orm_flutter/orm_flutter.dart', + _ => 'package:orm/engines/binary.dart', + }; + } + + String get engineName { + return switch (isFlutterEngineType(options.generator.config)) { + true => 'PrismaQueryEngine', + _ => 'BinaryEngine', + }; + } +} + +extension on String { + String literal() { + return replaceAll('\'', '\\\'') + .replaceAll('\n', '\\n') + .replaceAll('\r\n', '\n'); + } +} diff --git a/packages/orm/lib/engines/binary.dart b/packages/orm/lib/engines/binary.dart index 93c2f2ca..6e0c2597 100644 --- a/packages/orm/lib/engines/binary.dart +++ b/packages/orm/lib/engines/binary.dart @@ -1,6 +1,3 @@ -/// TODO: Remove in `v5.x` version. -@Deprecated( - 'Deprecated on `v5.x` version, use `import \'package:orm/orm.dart\'') library prisma.engines.binary; -export '../src/runtime/engines/binary_engine.dart'; +export '../src/engines/binary_engine.dart'; diff --git a/packages/orm/lib/orm.dart b/packages/orm/lib/orm.dart index 1c6cf3ae..d47d1ad2 100644 --- a/packages/orm/lib/orm.dart +++ b/packages/orm/lib/orm.dart @@ -29,5 +29,14 @@ export 'src/runtime/metrics/metrics_format.dart'; // Raw export 'src/runtime/raw/raw_client.dart'; -// Engines -export 'src/runtime/engines/binary_engine.dart'; +// v5.0 +export 'src/base_prisma_client.dart'; +export 'src/logging.dart'; +export 'src/prisma_client_options.dart'; +export 'src/prisma_namespace.dart'; + +export 'src/config/prisma+env.dart'; +export 'src/config/prisma+config.dart'; + +export 'src/datasources/datasources.dart'; +export 'src/datasources/prisma+datasource_utils.dart'; diff --git a/packages/orm/lib/src/_internal/project_directory.dart b/packages/orm/lib/src/_internal/project_directory.dart index 0b2a7aa5..474977f9 100644 --- a/packages/orm/lib/src/_internal/project_directory.dart +++ b/packages/orm/lib/src/_internal/project_directory.dart @@ -1,16 +1,20 @@ import 'dart:io'; -import 'package:path/path.dart'; +import 'package:path/path.dart' as path; -final Directory projectDirectory = _findProjectDirectory(Directory.current); +Directory? findProjectDirectory() => + _nestFindPubspecDirectory(File.fromUri(Platform.script).parent); -Directory _findProjectDirectory(Directory directory) { - final pubspec = File(join(directory.path, 'pubspec.yaml')); - if (pubspec.existsSync() || _isOsRoot(directory)) { - return directory; - } +Directory? _nestFindPubspecDirectory(Directory directory) { + try { + final pubspec = File(path.join(directory.path, 'pubspec.yaml')); + if (pubspec.existsSync()) return directory; + if (_isOsRoot(directory)) return null; - return _findProjectDirectory(directory.parent); + return _nestFindPubspecDirectory(directory.parent); + } catch (_) { + return null; + } } bool _isOsRoot(Directory directory) { @@ -18,5 +22,5 @@ bool _isOsRoot(Directory directory) { return directory.path.endsWith(':\\'); } - return relative(directory.path, from: '/') == '.'; + return path.relative(directory.path, from: '/') == '.'; } diff --git a/packages/orm/lib/src/base_prisma_client.dart b/packages/orm/lib/src/base_prisma_client.dart index 0ea6cfe1..f1fa77c0 100644 --- a/packages/orm/lib/src/base_prisma_client.dart +++ b/packages/orm/lib/src/base_prisma_client.dart @@ -23,4 +23,7 @@ abstract class BasePrismaClient { /// Returns the [Engine] instance typeof [E]. E get $engine; + + Future $connect() => $engine.start(); + Future $disconnect() => $engine.stop(); } diff --git a/packages/orm/lib/src/config/_loader.dart b/packages/orm/lib/src/config/_loader.dart new file mode 100644 index 00000000..53743276 --- /dev/null +++ b/packages/orm/lib/src/config/_loader.dart @@ -0,0 +1,3 @@ +import 'package:rc/loaders/environment.dart'; + +const loader = EnvironmentLoader(); diff --git a/packages/orm/lib/src/config/_loader.io.dart b/packages/orm/lib/src/config/_loader.io.dart new file mode 100644 index 00000000..b8e5e465 --- /dev/null +++ b/packages/orm/lib/src/config/_loader.io.dart @@ -0,0 +1,9 @@ +import 'package:path/path.dart' as path; +import 'package:rc/loaders/dotenv.dart'; + +final loader = DotenvLoader( + files: ['.env', path.join('prisma', '.env')], + includeSystemEnvironment: true, + skipMultipleLocations: true, + searchProjectDirectory: true, +); diff --git a/packages/orm/lib/src/config/prisma+config.dart b/packages/orm/lib/src/config/prisma+config.dart new file mode 100644 index 00000000..96d3061e --- /dev/null +++ b/packages/orm/lib/src/config/prisma+config.dart @@ -0,0 +1,18 @@ +// ignore_for_file: file_names + +import 'package:rc/rc.dart'; + +import '../prisma_namespace.dart'; +import '_loader.dart' if (dart.library.io) '_loader.io.dart'; + +extension Prisma$Config on PrismaNamespace { + static final _config = () { + return RC( + shouldWarn: true, + loaders: {loader}, + ); + }(); + + /// Returns prisma config + RC get config => _config; +} diff --git a/packages/orm/lib/src/config/prisma+env.dart b/packages/orm/lib/src/config/prisma+env.dart new file mode 100644 index 00000000..1d7a25a0 --- /dev/null +++ b/packages/orm/lib/src/config/prisma+env.dart @@ -0,0 +1,26 @@ +// ignore_for_file: file_names + +import 'package:rc/rc.dart'; + +import '../prisma_namespace.dart'; +import 'prisma+config.dart'; + +extension Prisma$Environment on PrismaNamespace { + bool get _skipDotiableKeys => envAsBoolean('prisma.env.skip_dotiable_keys'); + + /// Returns environment KV parts. + Map get environment => + config.toEnvironmentMap(skipDotiableKeys: _skipDotiableKeys); + + /// Returns a environment value by [name]. + String? env(String name) => config.env(name); + + /// Returns bool env value. + bool envAsBoolean(String name) { + final value = config(name); + return switch (value) { + true || "on" || "true" || 1 || "1" => true, + _ => false, + }; + } +} diff --git a/packages/orm/lib/src/datasources/_validate_datasource_url.dart b/packages/orm/lib/src/datasources/_validate_datasource_url.dart new file mode 100644 index 00000000..71e8da1e --- /dev/null +++ b/packages/orm/lib/src/datasources/_validate_datasource_url.dart @@ -0,0 +1,33 @@ +import '../errors.dart'; + +const _allowdProtocels = [ + 'mysql', + 'postgresql', + 'mongodb', + 'sqlserver' +]; + +PrismaClientInitializationError _createInvalidDatasourceError(String message) => + PrismaClientInitializationError(errorCode: 'P1013', message: message); + +String validateDatasourceURL(String datasourceUrl, {bool isPorxy = false}) { + final url = Uri.tryParse(datasourceUrl); + + if (isPorxy && url?.scheme == 'prisma') { + return url.toString(); + } else if (isPorxy) { + throw _createInvalidDatasourceError( + "Proxy connection URL must use the `prisma://` protocol"); + } + + if (url?.scheme == 'file') { + throw _createInvalidDatasourceError( + "The current platform does not support SQLite, please change the datasource URL"); + } + + if (url == null || !_allowdProtocels.contains(url.scheme)) { + throw _createInvalidDatasourceError("Invalid Prisma datasource URL"); + } + + return url.toString(); +} diff --git a/packages/orm/lib/src/datasources/_validate_datasource_url.io.dart b/packages/orm/lib/src/datasources/_validate_datasource_url.io.dart new file mode 100644 index 00000000..3616d17a --- /dev/null +++ b/packages/orm/lib/src/datasources/_validate_datasource_url.io.dart @@ -0,0 +1,23 @@ +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import '../errors.dart'; +import '_validate_datasource_url.dart' as shared; + +String validateDatasourceURL(String datasourceUrl, {bool isPorxy = false}) { + final url = Uri.tryParse(datasourceUrl); + if (url?.scheme != 'file' || url == null) { + return shared.validateDatasourceURL(datasourceUrl, isPorxy: isPorxy); + } + + final databaseFile = File.fromUri(url); + final databasePath = path.relative(databaseFile.path); + if (databaseFile.existsSync()) { + return 'file:$databasePath'; + } + + throw PrismaClientInitializationError( + errorCode: "P1003", + message: "The SQLite database file ($databasePath) does not exist"); +} diff --git a/packages/orm/lib/src/datasources/datasources.dart b/packages/orm/lib/src/datasources/datasources.dart new file mode 100644 index 00000000..6a07ecaf --- /dev/null +++ b/packages/orm/lib/src/datasources/datasources.dart @@ -0,0 +1,10 @@ +enum DatasourceType { url, enviroment } + +class Datasource { + final DatasourceType type; + final String value; + + const Datasource(this.type, this.value); +} + +typedef Datasources = Map; diff --git a/packages/orm/lib/src/datasources/prisma+datasource_utils.dart b/packages/orm/lib/src/datasources/prisma+datasource_utils.dart new file mode 100644 index 00000000..57e8104d --- /dev/null +++ b/packages/orm/lib/src/datasources/prisma+datasource_utils.dart @@ -0,0 +1,12 @@ +// ignore_for_file: file_names + +import '../prisma_namespace.dart'; +import '_validate_datasource_url.dart' + if (dart.library.io) '_validate_datasource_url.io.dart' + as validate_datasource_url; + +extension Prisma$DatasourceUtils on PrismaNamespace { + String validateDatasourceURL(String datasourceUrl, {bool isPorxy = false}) => + validate_datasource_url.validateDatasourceURL(datasourceUrl, + isPorxy: isPorxy); +} diff --git a/packages/orm/lib/src/engines/binary_engine.dart b/packages/orm/lib/src/engines/binary_engine.dart new file mode 100644 index 00000000..045ff35f --- /dev/null +++ b/packages/orm/lib/src/engines/binary_engine.dart @@ -0,0 +1,439 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as path; +import 'package:retry/retry.dart'; +import 'package:webfetch/webfetch.dart' show fetch; + +import '../_internal/project_directory.dart'; +import '../datasources/datasources.dart'; +import '../datasources/prisma+datasource_utils.dart'; +import '../errors.dart'; +import '../logging.dart'; +import '../prisma_client_options.dart'; +import '../prisma_namespace.dart'; +import '../config/prisma+env.dart'; +import '../runtime/engine.dart'; +import '../runtime/json_protocol/deserialize.dart'; +import '../runtime/json_protocol/protocol.dart'; +import '../runtime/metrics/metrics_format.dart'; +import '../runtime/transaction/isolation_level.dart'; +import '../runtime/transaction/transaction.dart'; +import '../runtime/transaction/transaction_headers.dart'; + +/// Prisma branary engine. +class BinaryEngine extends Engine { + late Uri _endpoint; + Future Function()? _stopCallback; + + /// Creates a new [BinaryEngine]. + BinaryEngine({ + required super.schema, + required super.datasources, + required super.options, + }); + + @override + Future start() async { + if (_stopCallback != null) return; + + final (endpoint, stop) = await createServer(); + + _endpoint = endpoint; + _stopCallback = stop; + } + + @override + Future stop() async => _stopCallback?.call(); + + @override + Future request( + JsonQuery query, { + TransactionHeaders? headers, + Transaction? transaction, + }) async { + headers ??= TransactionHeaders(); + await start(); + + if (transaction != null) { + headers.set('x-transaction-id', transaction.id); + } + + final response = await fetch( + _endpoint, + headers: headers, + body: query.toJson(), + method: 'POST', + ); + + final result = await response.json(); + + return switch (result) { + {'data': final Map data} => deserializeJsonResponse(data), + {'errors': final Iterable errors} => throw Exception(errors), + _ => throw Exception(result), + }; + } + + @override + Future commitTransaction({ + required TransactionHeaders headers, + required Transaction transaction, + }) async { + await start(); + + final response = await fetch( + _endpoint.resolve('/transaction/${transaction.id}/commit'), + method: 'POST', + headers: headers, + ); + final result = await response.json(); + + return switch (result) { + {'errors': final Iterable errors} => throw Exception(errors), + _ => null, + }; + } + + @override + Future rollbackTransaction({ + required TransactionHeaders headers, + required Transaction transaction, + }) async { + await start(); + + final response = await fetch( + _endpoint.resolve('/transaction/${transaction.id}/rollback'), + method: 'POST', + headers: headers, + ); + final result = await response.json(); + + return switch (result) { + {'errors': final Iterable errors} => throw Exception(errors), + _ => null, + }; + } + + @override + Future startTransaction({ + required TransactionHeaders headers, + int maxWait = 2000, + int timeout = 5000, + TransactionIsolationLevel? isolationLevel, + }) async { + await start(); + + final response = await fetch( + _endpoint.resolve('/transaction/start'), + method: 'POST', + headers: headers, + body: { + 'max_wait': maxWait, + 'timeout': timeout, + if (isolationLevel != null) 'isolation_level': isolationLevel.name, + }, + ); + final result = await response.json(); + + return switch (result) { + {'id': final String id} => Transaction(id), + {'errors': final Iterable errors} => throw Exception(errors), + _ => throw Exception(result), + }; + } + + @override + Future metrics({ + Map? globalLabels, + required MetricsFormat format, + }) async { + await start(); + + final response = await fetch( + _endpoint.replace( + path: '/metrics', + queryParameters: {'format': format.name}, + ), + method: 'POST', + body: globalLabels, + ); + + return switch (format) { + MetricsFormat.json => response.json(), + MetricsFormat.prometheus => response.text(), + }; + } +} + +extension on BinaryEngine { + /// Find query engine. + File findQueryEngine() { + const executable = 'prisma-query-engine'; + final projectDirectory = findProjectDirectory(); + + Iterable generateEnginePaths( + Iterable searchDirectories) sync* { + for (final dir in searchDirectories) { + yield path.join(dir.path, executable); + yield path.join(dir.path, 'prisma', executable); + yield path.join(dir.path, '.dart_tool', executable); + } + } + + final enginePaths = generateEnginePaths([ + Directory.current, + if (projectDirectory != null) projectDirectory, + ]); + + for (final enginePath in enginePaths) { + final file = File(enginePath); + if (file.existsSync()) return file; + } + + throw PrismaClientInitializationError( + errorCode: "QE404", + message: + 'No binary engine found, please make sure any of the following locations contain the executable file: $enginePaths'); + } + + /// Creates overwrite datasources string. + String createOverwriteDatasourcesString() { + Map overwriteDatasources = + this.datasources.map((name, datasource) { + final url = switch (datasource) { + Datasource(type: DatasourceType.url, value: final url) => url, + Datasource(type: DatasourceType.enviroment, value: final name) => + Prisma.env(name).or( + () => throw PrismaClientInitializationError( + errorCode: "P1013", + message: 'The environment variable "$name" does not exist', + ), + ), + }; + + return MapEntry(name, Prisma.validateDatasourceURL(url, isPorxy: false)); + }); + + if (options.datasources?.isNotEmpty == true) { + overwriteDatasources.addAll(options.datasources!); + } + + if (options.datasourceUrl != null) { + final url = + Prisma.validateDatasourceURL(options.datasourceUrl!, isPorxy: false); + overwriteDatasources = + overwriteDatasources.map((name, _) => MapEntry(name, url)); + } + + final datasources = overwriteDatasources.entries + .map((e) => {'name': e.key, 'url': e.value}) + .toList(); + + return base64.encode(utf8.encode(json.encode(datasources))); + } + + /// Create engine args + Iterable createQueryEngineArgs() sync* { + yield '--enable-metrics'; + yield '--enable-raw-queries'; + yield* ['--engine-protocol', 'json']; + + // Set port is dynamic free port + yield* ['--port', '0']; + + // Sets datamodel + yield '--datamodel'; + yield base64.encode(utf8.encode(schema)); + + // Set overwrite datasources + yield '--overwrite-datasources'; + yield createOverwriteDatasourcesString(); + } + + Map createQueryEngineEnvironment() { + final environment = Map.from(Prisma.environment); + + // log queries + if (options.logEmitter.definition.any((e) => e.$1 == LogLevel.query)) { + environment['LOG_QUERIES'] = 'true'; + } + + // no color + if (!Prisma.envAsBoolean('NO_COLOR') && + options.errorFormat == ErrorFormat.pretty) { + environment['CLICOLOR_FORCE'] = "1"; + } + + environment['RUST_BACKTRACE'] = Prisma.env('RUST_BACKTRACE').or(() => '1'); + environment['RUST_LOG'] = + Prisma.env('RUST_LOG').or(() => LogLevel.info.name); + + return environment; + } + + Future<(Uri, Future Function())> createServer() async { + String executable = path.join( + '.', + path.relative(findQueryEngine().path, from: Directory.current.path), + ); + final arguments = createQueryEngineArgs().toList(); + final environment = createQueryEngineEnvironment(); + final process = await Process.start( + executable, + arguments, + workingDirectory: Directory.current.path, + includeParentEnvironment: false, + environment: environment, + ); + + final stderrSubscription = process.stderr.byline().listen((event) { + final payload = tryParseJSON(event); + if (payload + case { + 'error_code': final String errorCode, + 'message': final String message + }) { + process.kill(); + + throw PrismaClientInitializationError( + message: message, errorCode: errorCode); + } + }); + + final endpointCompleter = Completer.sync(); + final stdoutSubscription = process.stdout.byline().listen((event) { + final payload = tryParseJSON(event); + tryCompleteEndpoint(endpointCompleter, payload); + + if (payload case {'span': true, 'spans': final List _}) { + // TODO: Tracing engine span. current print payload to console. + print('EngineSpan:payload $payload'); + + return; + } + + final (level, engineEvent) = createEngineEvent(payload); + if (level == LogLevel.error && + engineEvent is LogEvent && + engineEvent.message.contains('fatal error')) { + process.kill(); + + throw PrismaClientRustPanicError(message: engineEvent.message); + } + + options.logEmitter.emit(level, engineEvent); + }); + + Future stop() async { + process.kill(); + await stderrSubscription.cancel(); + await stdoutSubscription.cancel(); + } + + final endpoint = await retry(() async { + return endpointCompleter.future.timeout(Duration(seconds: 1), + onTimeout: () { + throw PrismaClientInitializationError( + message: 'Prisma binary query engine not ready'); + }); + }).catchError((e) { + stop(); + throw e; + }); + + return (endpoint, stop); + } + + (LogLevel, EngineEvent) createEngineEvent(payload) { + if (payload + case { + 'timestamp': final String timestamp, + 'target': final String target, + 'fields': { + 'query': final String query, + 'params': final String params, + 'duration': final num duration, + }, + }) { + final event = QueryEvent( + timestamp: DateTime.parse(timestamp), + target: target, + query: query, + params: params, + duration: Duration(milliseconds: duration.toInt()), + ); + + return (LogLevel.query, event); + } else if (payload + case { + 'timestamp': final String timestamp, + 'target': final String target, + 'level': final String rawLogLevel, + 'fields': {'message': final String message} + }) { + final level = LogLevel.values.firstWhere( + (e) => e.name.toLowerCase() == rawLogLevel.toLowerCase(), + orElse: () => LogLevel.warn); + final event = LogEvent( + timestamp: DateTime.parse(timestamp), + target: target, + message: message, + ); + + return (level, event); + } + + return ( + LogLevel.warn, + LogEvent( + target: 'prisma:client:engines:binary', + timestamp: DateTime.now(), + message: 'Parse event fail, raw event: ${json.encode(payload)}', + ) + ); + } + + void tryCompleteEndpoint(Completer completer, payload) { + if (payload + case { + 'level': 'INFO', + 'target': 'query_engine::server', + 'fields': { + 'message': final String message, + 'ip': final String ip, + 'port': final String port, + } + } + when message.startsWith('Started query engine http server') && + ip.isNotEmpty && + port.isNotEmpty && + !completer.isCompleted) { + final endpoint = Uri.http('$ip:$port'); + completer.complete(endpoint); + } + } + + Object? tryParseJSON(String encoded) { + try { + return json.decode(encoded); + } catch (e) { + // TODO, debug + return null; + } + } +} + +extension on T? { + T or(T Function() fn) { + if (this != null) return this as T; + + return fn(); + } +} + +extension on Stream> { + Stream byline() => + transform(utf8.decoder).transform(const LineSplitter()); +} diff --git a/packages/orm/lib/src/env/_create_environment.io.dart b/packages/orm/lib/src/env/_create_environment.io.dart deleted file mode 100644 index a5d53256..00000000 --- a/packages/orm/lib/src/env/_create_environment.io.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'dart:io'; - -import 'package:path/path.dart' as path; -import 'package:dotenv/dotenv.dart' show DotEnv; - -import '_env.shared.dart' as shared; - -class _IOEnvironmentImpl implements shared.Environment { - final shared.Environment env; - final DotEnv dotenv; - - const _IOEnvironmentImpl(this.env, this.dotenv); - - @override - void add(Map other) => env.add(other); - - @override - String? call(String name) => env(name) ?? dotenv[name]; -} - -shared.Environment createEnvironment() { - final env = shared.createEnvironment(); - env.add(Platform.environment); - - final dotenv = DotEnv(includePlatformEnvironment: false, quiet: true); - dotenv.load(_envFiles()); - - return _IOEnvironmentImpl(env, dotenv); -} - -Iterable _envFiles() sync* { - final dirs = [ - Directory.current.path, - path.dirname(Platform.script.toFilePath(windows: Platform.isWindows)), - ]; - for (final dir in dirs) { - yield path.join(dir, '.env'); - yield path.join(dir, 'prisma', '.env'); - } -} diff --git a/packages/orm/lib/src/env/_env.shared.dart b/packages/orm/lib/src/env/_env.shared.dart deleted file mode 100644 index c285225f..00000000 --- a/packages/orm/lib/src/env/_env.shared.dart +++ /dev/null @@ -1,21 +0,0 @@ -/// Prisma environment. -class Environment { - Environment._(); - - final _enviroment = {}; - - /// Returns a enviroment value by [name], if the [name] - /// not contains, return [null]. - String? call(String name) { - if (bool.hasEnvironment(name)) return String.fromEnvironment(name); - if (_enviroment.containsKey(name)) return _enviroment[name]; - - return null; - } - - /// Add [other] to the enviroment. - void add(Map other) => _enviroment.addAll(other); -} - -/// Internal, create enviroment. -Environment createEnvironment() => Environment._(); diff --git a/packages/orm/lib/src/env/env.dart b/packages/orm/lib/src/env/env.dart deleted file mode 100644 index 916bd7fd..00000000 --- a/packages/orm/lib/src/env/env.dart +++ /dev/null @@ -1,68 +0,0 @@ -import '_env.shared.dart' show Environment; -import '_env.shared.dart' if (dart.library.io) '_create_environment.io.dart' - show createEnvironment; - -export '_env.shared.dart' show Environment; - -/// Enviroment -/// -/// Match stack: -/// 1. `bool.hasEnviroment`, Using the `--define` key-value part. -/// 2. Internal enviroment map, contains key, if extsis return it. -/// 3. load `.env` from `/.env` or `/prisma.env` file. -/// -/// Example: -/// ```dart -/// // Read a env value. -/// final debug = env('DEBUG'); // String or null -/// -/// // Add other env -/// env.add({'HI': "Hello"}); -/// final hi = env('HI'); // "Hello" -/// ``` -final env = createEnvironment(); - -extension EnvironmentUtils on Environment { - /// Returns a value parsed to [bool] type. - /// - /// If the value is `true` | `on` | `1` return `true`, other reurn `false` value. - bool boolean(String name) { - const trueStrings = ['true', '1', 'on']; - - return trueStrings.contains(this(name)); - } - - /// Returns a value parsed to [num] type. - /// - /// Try parse string value, if faild return `0` - num number(String name) { - final value = this(name); - if (value == null) return 0; - - final number = num.tryParse(value); - if (number != null) return number; - - final bigint = BigInt.tryParse(value); - if (bigint != null) bigint.toDouble(); - - return 0; - } - - /// Configure the Prisma client and engine. - /// - /// - [debug]: `DEBUG` is used to enable debugging output in Prisma Client. @see https://www.prisma.io/docs/orm/prisma-client/debugging-and-troubleshooting/debugging - /// - [noColor]: @see https://www.prisma.io/docs/orm/reference/environment-variables-reference#no_color - /// - [queryEngineBinary]: Custom binary query engine location, Only used by generators and binary engine. See https://www.prisma.io/docs/orm/reference/environment-variables-reference#prisma_query_engine_binary - void configure(String? debug, bool? noColor, String? queryEngineBinary) { - final configure = { - if (debug != null) 'DEBUG': debug, - if (noColor != null) 'NO_COLOR': noColor ? 'true' : 'false', - if (queryEngineBinary != null) - 'PRISMA_QUERY_ENGINE_BINARY': queryEngineBinary, - }; - - if (configure.isNotEmpty) { - add(configure); - } - } -} diff --git a/packages/orm/lib/src/errors.dart b/packages/orm/lib/src/errors.dart index 9b38647e..f9dc72fa 100644 --- a/packages/orm/lib/src/errors.dart +++ b/packages/orm/lib/src/errors.dart @@ -10,6 +10,9 @@ abstract class PrismaClientError extends Error { /// Creates a new Prisma client error. PrismaClientError({required this.message, this.clientVersion = version}); + + @override + toString() => '$runtimeType: $message'; } /// Prisma Client throws a [PrismaClientValidationError] exception if validation fails - for example: @@ -46,10 +49,13 @@ class PrismaClientRustPanicError extends PrismaClientError { /// * The query engine binary for the current platform could not be found (`generator` block) class PrismaClientInitializationError extends PrismaClientError { /// A Prisma-specific error code. - final String errorCode; + final String? errorCode; PrismaClientInitializationError( - {required this.errorCode, required super.message, super.clientVersion}); + {this.errorCode, required super.message, super.clientVersion}); + + @override + toString() => '$runtimeType: $errorCode $message'; } /// Prisma Client throws a [PrismaClientKnownRequestError] exception if the query engine returns a known error related to the request - for example, a unique constraint violation. @@ -70,4 +76,7 @@ class PrismaClientKnownRequestError extends PrismaClientError { required super.message, super.clientVersion, this.meta}); + + @override + toString() => '$runtimeType: $code $message'; } diff --git a/packages/orm/lib/src/logging.dart b/packages/orm/lib/src/logging.dart index 2783e4a1..3cbf0489 100644 --- a/packages/orm/lib/src/logging.dart +++ b/packages/orm/lib/src/logging.dart @@ -31,7 +31,7 @@ final class LogEvent extends EngineEvent { final class QueryEvent extends EngineEvent { final String query; final String params; - final double duration; + final Duration duration; const QueryEvent( {required super.timestamp, @@ -46,10 +46,11 @@ typedef LogListener = void Function(EngineEvent event); /// Prisma log event emitter. class LogEmitter { - final LogDefinition _definition; + final LogDefinition definition; final _listeners = <(LogLevel, LogListener)>[]; - LogEmitter(LogDefinition definition) : _definition = definition; + LogEmitter(LogDefinition definition) + : definition = LogDefinition.unmodifiable(definition); void on(LogLevel level, LogListener listener) { final extsis = _listeners.any((e) => e.$1 == level && e.$2 == listener); @@ -64,7 +65,7 @@ class LogEmitter { } void _emitStdout(LogLevel level, EngineEvent event) { - if (_definition.should(level, LogEmit.stdout)) { + if (definition.should(level, LogEmit.stdout)) { final name = 'prisma:${level.name}'; final message = switch (event) { LogEvent(message: final String message) => message, @@ -78,7 +79,7 @@ class LogEmitter { } void _emitEvent(LogLevel level, EngineEvent event) { - if (!_definition.should(level, LogEmit.event)) return; + if (!definition.should(level, LogEmit.event)) return; final listeners = _listeners.where((e) => e.$1 == level).map((e) => e.$2); for (final listener in listeners) { diff --git a/packages/orm/lib/src/prisma_namespace.dart b/packages/orm/lib/src/prisma_namespace.dart new file mode 100644 index 00000000..059f9aa9 --- /dev/null +++ b/packages/orm/lib/src/prisma_namespace.dart @@ -0,0 +1,8 @@ +/// Prisma namespace utils +class PrismaNamespace { + const PrismaNamespace._(); +} + +/// Priama Namespace, About Saving Public Tool Collections. +/// ignore: constant_identifier_names +const Prisma = PrismaNamespace._(); diff --git a/packages/orm/lib/src/runtime/engine.dart b/packages/orm/lib/src/runtime/engine.dart index 3124d906..2c130dde 100644 --- a/packages/orm/lib/src/runtime/engine.dart +++ b/packages/orm/lib/src/runtime/engine.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import '../datasources/datasources.dart'; import '../prisma_client_options.dart'; import 'json_protocol/protocol.dart'; import 'metrics/metrics_format.dart'; @@ -7,28 +8,16 @@ import 'transaction/isolation_level.dart'; import 'transaction/transaction_headers.dart'; import 'transaction/transaction.dart'; -enum DatasourceType { url, enviroment } - -class Datasource { - final DatasourceType type; - final String value; - - const Datasource(this.type, this.value); -} - -typedef Datasources = Map; - abstract class Engine { final String schema; final Datasources datasources; - final Datasources? overwriteDatasources; + final PrismaClientOptions options; const Engine({ required this.options, required this.schema, required this.datasources, - this.overwriteDatasources, }); /// Starts the engine. diff --git a/packages/orm/lib/src/runtime/engines/binary_engine.dart b/packages/orm/lib/src/runtime/engines/binary_engine.dart deleted file mode 100644 index 30e45e1c..00000000 --- a/packages/orm/lib/src/runtime/engines/binary_engine.dart +++ /dev/null @@ -1,295 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; -import 'package:retry/retry.dart'; -import 'package:webfetch/webfetch.dart' show fetch; - -import '../engine.dart'; -import '../json_protocol/deserialize.dart'; -import '../json_protocol/protocol.dart'; -import '../metrics/metrics_format.dart'; -import '../transaction/isolation_level.dart'; -import '../transaction/transaction.dart'; -import '../transaction/transaction_headers.dart'; - -/// Prisma branary engine. -class BinaryEngine extends Engine { - Process? _process; - Uri? _endpoint; - final logger = Logger('prisma.binary-engine'); - - /// Creates a new [BinaryEngine]. - BinaryEngine({ - required super.schema, - required super.datasources, - required super.options, - super.overwriteDatasources, - }); - - /// Search for prisma binary query engine path. - File get _executable { - final executable = 'prisma-query-engine'; - final searchDirectories = [ - Directory.current.path, - path.join(Directory.current.path, 'prisma'), - path.join(Directory.current.path, '.dart_tool'), - ]; - - for (final directory in searchDirectories) { - final file = File(path.join(directory, executable)); - if (file.existsSync()) { - logger.info('Found query engine binary in ${file.path}'); - return file; - } - } - - logger.severe('No query engine binary found in $searchDirectories'); - throw Exception( - 'No query engine binary found ($executable) in $searchDirectories'); - } - - /// Generate owerwrite datasources. - String get _overwriteDatasources { - final overwrite = datasources.entries - .map((e) => {'name': e.key, 'url': e.value}) - .toList(); - - return base64.encode(utf8.encode(json.encode(overwrite))); - } - - @override - Future start() async { - _process ??= await _internalStart(); - } - - Future _internalStart() async { - if (_process != null) return _process!; - - final arguments = [ - '--enable-metrics', - '--enable-open-telemetry', - '--enable-raw-queries', - ]; - - // Set port. - arguments.addAll(['--port', '0']); - - // Set --datamodel - arguments.addAll(['--datamodel', base64.encode(utf8.encode(schema))]); - - // Set protocol is json - arguments.addAll(['--engine-protocol', 'json']); - - // Set overwrite datasources - arguments.addAll(['--overwrite-datasources', _overwriteDatasources]); - - final process = await Process.start( - path.join( - '.', - path.relative(_executable.path, from: path.current), - ), - arguments, - workingDirectory: path.current, - includeParentEnvironment: true, - environment: { - 'RUST_BACKTRACE': '1', - 'RUST_LOG': 'info', - }, - ); - - _listenProcess(process); - final ready = await _serverReady(); - if (!ready) { - process.kill(); - throw Exception('Unable to start query engine'); - } - - return process; - } - - Future _serverEndpoint() async { - // Await _endpoint is not null - // With `retry` package, retry 5 times with 1 second delay - return retry( - () => _endpoint != null - ? _endpoint! - : throw Exception('Prisma binary query engine not ready'), - ); - } - - /// Server ready - Future _serverReady() async { - // Check if server is ready - final url = (await _serverEndpoint()).resolve('/status'); - - return retry(() async { - final response = await fetch(url); - final data = await response.json(); - - if (data case {'status': 'ok'}) { - return true; - } - - final error = Exception('Prisma binary query engine not ready'); - logger.severe('Prisma binary query engine not ready ($url)', error); - - throw error; - }); - } - - /// Listen process - void _listenProcess(Process process) { - final stream = - process.stdout.transform(utf8.decoder).transform(const LineSplitter()); - stream.listen((message) { - if (message.isEmpty) return; - try { - final data = json.decode(message); - if (data - case { - 'level': "INFO", - 'target': 'query_engine::server', - 'fields': {'ip': final String host, 'port': final String port} - }) { - _endpoint = Uri(scheme: 'http', host: host, port: int.parse(port)); - } - } catch (e, s) { - logger.severe('Unable to parse message: $message', e, s); - rethrow; - } - }); - } - - @override - Future stop() async { - _process?.kill(); - _process = null; - } - - @override - Future request( - JsonQuery query, { - TransactionHeaders? headers, - Transaction? transaction, - }) async { - headers ??= TransactionHeaders(); - - await start(); - final endpoint = await _serverEndpoint(); - - if (transaction != null) { - headers.set('x-transaction-id', transaction.id); - } - - final response = await fetch( - endpoint, - headers: headers, - body: query.toJson(), - method: 'POST', - ); - - final result = await response.json(); - - return switch (result) { - {'data': final Map data} => deserializeJsonResponse(data), - {'errors': final Iterable errors} => throw Exception(errors), - _ => throw Exception(result), - }; - } - - @override - Future commitTransaction({ - required TransactionHeaders headers, - required Transaction transaction, - }) async { - await start(); - - final endpoint = await _serverEndpoint(); - final response = await fetch( - endpoint.resolve('/transaction/${transaction.id}/commit'), - method: 'POST', - headers: headers, - ); - final result = await response.json(); - - return switch (result) { - {'errors': final Iterable errors} => throw Exception(errors), - _ => null, - }; - } - - @override - Future rollbackTransaction({ - required TransactionHeaders headers, - required Transaction transaction, - }) async { - await start(); - - final endpoint = await _serverEndpoint(); - final response = await fetch( - endpoint.resolve('/transaction/${transaction.id}/rollback'), - method: 'POST', - headers: headers, - ); - final result = await response.json(); - - return switch (result) { - {'errors': final Iterable errors} => throw Exception(errors), - _ => null, - }; - } - - @override - Future startTransaction({ - required TransactionHeaders headers, - int maxWait = 2000, - int timeout = 5000, - TransactionIsolationLevel? isolationLevel, - }) async { - await start(); - final endpoint = await _serverEndpoint(); - final response = await fetch( - endpoint.resolve('/transaction/start'), - method: 'POST', - headers: headers, - body: { - 'max_wait': maxWait, - 'timeout': timeout, - if (isolationLevel != null) 'isolation_level': isolationLevel.name, - }, - ); - final result = await response.json(); - - return switch (result) { - {'id': final String id} => Transaction(id), - {'errors': final Iterable errors} => throw Exception(errors), - _ => throw Exception(result), - }; - } - - @override - Future metrics({ - Map? globalLabels, - required MetricsFormat format, - }) async { - await start(); - - final endpoint = await _serverEndpoint(); - final response = await fetch( - endpoint.replace( - path: '/metrics', - queryParameters: {'format': format.name}, - ), - method: 'POST', - body: globalLabels, - ); - - return switch (format) { - MetricsFormat.json => response.json(), - MetricsFormat.prometheus => response.text(), - }; - } -} diff --git a/packages/orm/lib/src/runtime/raw/raw_client.dart b/packages/orm/lib/src/runtime/raw/raw_client.dart index 59c68436..78f5e163 100644 --- a/packages/orm/lib/src/runtime/raw/raw_client.dart +++ b/packages/orm/lib/src/runtime/raw/raw_client.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import '../../base_prisma_client.dart'; import '../../dmmf/datamodel.dart' show DataModel; import '../engine.dart'; import '../json_protocol/protocol.dart'; @@ -7,7 +8,7 @@ import '../json_protocol/serialize.dart'; import '../transaction/transaction_client.dart'; import '_internal/raw_parameter_codec.dart'; -class RawClient { +class RawClient { final Engine _engine; final DataModel _datamodel; final TransactionClient _client; diff --git a/packages/orm/lib/src/runtime/transaction/transaction_client.dart b/packages/orm/lib/src/runtime/transaction/transaction_client.dart index ce787194..a663ac5b 100644 --- a/packages/orm/lib/src/runtime/transaction/transaction_client.dart +++ b/packages/orm/lib/src/runtime/transaction/transaction_client.dart @@ -1,9 +1,10 @@ +import '../../base_prisma_client.dart'; import '../engine.dart'; import 'isolation_level.dart'; import 'transaction_headers.dart'; import 'transaction.dart'; -class TransactionClient { +class TransactionClient { final Engine _engine; final T Function(TransactionClient transactionClient) _factory; diff --git a/packages/orm/pubspec.yaml b/packages/orm/pubspec.yaml index e4261fb6..4f9f7852 100644 --- a/packages/orm/pubspec.yaml +++ b/packages/orm/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: json_rpc_2: ^3.0.2 logging: ^1.2.0 path: ^1.8.3 - rc: ^0.3.0 + rc: ^0.3.2 recase: ^4.1.0 retry: ^3.1.2 stream_channel: ^2.1.2 From 5a9d5073668aba3ffa5f3ec245b00bc461ca25d2 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Thu, 6 Jun 2024 21:58:13 +0800 Subject: [PATCH 10/20] chore: WIP --- packages/orm/bin/orm.dart | 9 +- packages/orm/bin/src/generate_client.dart | 233 +++++------------- ...lient.dart.without_code_builder_test.dart} | 0 .../orm/lib/src/datasources/datasources.dart | 2 +- .../orm/lib/src/engines/binary_engine.dart | 2 +- 5 files changed, 63 insertions(+), 183 deletions(-) rename packages/orm/bin/src/{generator+client.dart => generator+client.dart.without_code_builder_test.dart} (100%) diff --git a/packages/orm/bin/orm.dart b/packages/orm/bin/orm.dart index 0366b7f7..e4ecb5e1 100644 --- a/packages/orm/bin/orm.dart +++ b/packages/orm/bin/orm.dart @@ -8,7 +8,7 @@ import 'package:path/path.dart'; import 'src/generator.dart'; import 'src/utils/is_flutter_engine_type.dart'; -import 'src/generator+client.dart'; +// import 'src/generator+client.dart'; void main() async { final app = GeneratorApp.stdio(stdin: stdin, stdout: stderr); @@ -54,13 +54,6 @@ Future generate(GeneratorOptions options) async { await output.writeAsString(formated); } - final client = formatter.format(generator.client()); - final file = - await File(join(options.generator.output!.value, 'new_client.dart')) - .autoCreate(); - - await file.writeAsString(client); - if (isFlutterEngineType(options.generator.config)) { // Copy prisma query engine. final engineDownloadPath = diff --git a/packages/orm/bin/src/generate_client.dart b/packages/orm/bin/src/generate_client.dart index 7506bfb8..9db4d238 100644 --- a/packages/orm/bin/src/generate_client.dart +++ b/packages/orm/bin/src/generate_client.dart @@ -2,6 +2,7 @@ import 'package:code_builder/code_builder.dart'; import 'package:orm/src/dmmf/mappings.dart'; +import 'package:orm/generator_helper.dart' as gh; import 'generate_delegate.dart'; import 'generator.dart'; @@ -14,18 +15,13 @@ extension GenerateClient on Generator { return Class((builder) { builder.name = 'PrismaClient'; - builder.fields.add(generateDatamodel()); - builder.fields.addAll([_metricsField, _transactionField, _engineField]); + builder.extend = refer('BasePrismaClient', 'package:orm/orm.dart'); - builder.methods.add(_connectMethod); - builder.methods.add(_disconnectMethod); + builder.fields.add(generateDatamodel()); + builder.fields.add(_engineField); builder.constructors.add(_defaultConstructor); - builder.constructors.add(generateUseFactory()); - - if (!isFlutterEngineType(options.generator.config)) { - builder.constructors.add(generatePublicFactory()); - } + builder.methods.add(createEngineGetter()); for (final mapping in options.dmmf.mappings.modelOperations) { final method = generateModelOperation(mapping); @@ -36,121 +32,52 @@ extension GenerateClient on Generator { .contains('queryRaw') && options.dmmf.mappings.otherOperations.write.contains('executeRaw'); if (allowRaw) { - builder.methods.add(_rawClientGetter); + // builder.methods.add(_rawClientGetter); } }); } } extension on Generator { - Constructor generateUseFactory() { - final datasource = Map.fromEntries( - options.datasources - .map((e) => {e.name: e.url.value}) - .expand((e) => e.entries), - ); - - return Constructor((builder) { - builder.factory = true; - builder.name = 'use'; - - builder.requiredParameters.add(Parameter((builder) { - builder.name = 'factory'; - builder.type = FunctionType((builder) { - builder.returnType = refer('Engine', 'package:orm/orm.dart'); - builder.requiredParameters.add(refer('String schema')); - builder.requiredParameters - .add(refer('Map datasources')); - }); - })); - - builder.body = Block.of([ - declareFinal('engine') - .assign(refer('factory').call([ - // TODO: https://github.com/dart-lang/code_builder/issues/452 - literalString(options.schema.replaceAll('\r\n', '\n')), - literalConstMap(datasource) - ])) - .statement, - declareFinal('metrics') - .assign( - refer('MetricsClient', 'package:orm/orm.dart') - .newInstance([refer('engine')]), - ) - .statement, - Method((builder) { - builder.name = 'createClientWithTransaction'; - builder.requiredParameters.add(Parameter((builder) { - builder.name = 'transaction'; - builder.type = TypeReference((builder) { - builder.symbol = 'TransactionClient'; - builder.url = 'package:orm/orm.dart'; - builder.types.add(refer('PrismaClient')); - }); - })); - builder.lambda = true; - builder.returns = refer('PrismaClient'); - builder.body = refer('PrismaClient').newInstanceNamed('_', [ - refer('engine'), - refer('transaction'), - refer('metrics'), - ]).statement; - }).closure.code, - declareFinal('transaction') - .assign(TypeReference((builder) { - builder.symbol = 'TransactionClient'; - builder.url = 'package:orm/orm.dart'; - builder.types.add(refer('PrismaClient')); - }).newInstance([ - refer('engine'), - refer('createClientWithTransaction'), - ])) - .statement, - refer('createClientWithTransaction') - .call([refer('transaction')]) - .returned - .statement, - ]); + Method createEngineGetter() { + final engine = switch (options.generator.config['engineType']) { + 'flutter' => + refer('PrismaFlutterEngine', 'package:orm_flutter/orm_flutter.dart'), + _ => refer('BinaryEngine', 'package:orm/engines/binary.dart'), + }; + + final datasources = options.datasources.map((e) { + final (value, type) = switch (e.url) { + gh.EnvVar(name: final String name) => ( + name, + 'DatasourceType.environment' + ), + gh.EnvValue(value: final String url) => (url, 'DatasourceType.url') + }; + + return MapEntry( + e.name, + refer('Datasource', 'package:orm/orm.dart').newInstance([ + refer(type, 'package:orm/orm.dart'), + literalString(value), + ])); }); - } - Constructor generatePublicFactory() { - return Constructor((builder) { - builder.factory = true; - - builder.optionalParameters.add(Parameter((builder) { - builder.name = 'datasourceUrl'; - builder.named = true; - builder.type = refer('String').nullable(true); - })); - - builder.optionalParameters.add(Parameter((builder) { - builder.name = 'datasources'; - builder.named = true; - builder.type = refer('Map').nullable(true); - })); - - builder.body = refer('PrismaClient').property('use').call([ - Method((builder) { - builder.requiredParameters.add(Parameter((p) => p.name = 'schema')); - builder.requiredParameters - .add(Parameter((p) => p.name = 'defaultDataSources')); - - final engine = - refer('BinaryEngine', 'package:orm/engines/binary.dart') - .newInstance([], { - 'schema': refer('schema'), - 'datasources': refer('datasourceOverrides'), - }); - - builder.body = Block.of([ - Code( - 'final datasourceOverrides = (datasources ?? defaultDataSources)' - '.map((k, v) => MapEntry(k, datasourceUrl ?? v));'), - engine.returned.statement, - ]); - }).closure - ]).code; + return Method((builder) { + builder.name = '\$engine'; + builder.lambda = true; + builder.annotations.add(refer('override')); + builder.type = MethodType.getter; + builder.body = refer('_engine') + .assignNullAware( + engine.newInstance([], { + // TODO: https://github.com/dart-lang/code_builder/issues/452 + 'schema': literalString(options.schema.replaceAll('\r\n', '\n')), + 'datasources': literalConstMap(Map.fromEntries(datasources)), + 'options': refer('\$options'), + }), + ) + .code; }); } @@ -180,73 +107,33 @@ extension on Generator { } final _defaultConstructor = Constructor((builder) { - builder.name = '_'; - builder.constant = true; + builder.constant = false; - final parameters = [ - _engineField.name, - _transactionField.name, - _metricsField.name + const superPptionalParameters = [ + 'datasourceUrl', + 'datasources', + 'errorFormat', + 'log' ]; - for (final parameter in parameters) { - builder.requiredParameters.add(Parameter((builder) { + for (final parameter in superPptionalParameters) { + builder.optionalParameters.add(Parameter((builder) { builder.name = parameter; - builder.toThis = true; builder.named = true; + builder.toSuper = true; })); } -}); -final _connectMethod = Method((builder) { - builder.name = '\$connect'; - builder.returns = refer('Future'); - builder.lambda = true; - builder.body = refer(_engineField.name).property('start').call([]).code; -}); + // engine parameter + builder.optionalParameters.add(Parameter((builder) { + builder.name = 'engine'; + builder.named = true; + builder.type = Reference('Engine?', 'package:orm/orm.dart'); + })); -final _disconnectMethod = Method((builder) { - builder.name = '\$disconnect'; - builder.returns = refer('Future'); - builder.lambda = true; - builder.body = refer(_engineField.name).property('stop').call([]).code; + builder.initializers.add(Code('_engine = engine')); }); final _engineField = Field((builder) { builder.name = '_engine'; - builder.modifier = FieldModifier.final$; - builder.type = refer('Engine', 'package:orm/orm.dart'); -}); - -final _transactionField = Field((builder) { - builder.modifier = FieldModifier.final$; - builder.name = '\$transaction'; - builder.type = TypeReference((builder) { - builder.symbol = 'TransactionClient'; - builder.url = 'package:orm/orm.dart'; - builder.types.add(refer('PrismaClient')); - }); -}); - -final _rawClientGetter = Method((builder) { - final client = TypeReference((builder) { - builder.symbol = 'RawClient'; - builder.url = 'package:orm/orm.dart'; - builder.types.add(refer('PrismaClient')); - }); - - builder.name = '\$raw'; - builder.type = MethodType.getter; - builder.returns = client; - builder.lambda = true; - builder.body = client.newInstance([ - refer(_engineField.name), - refer('datamodel'), - refer(_transactionField.name) - ]).code; -}); - -final _metricsField = Field((builder) { - builder.name = '\$metrics'; - builder.modifier = FieldModifier.final$; - builder.type = refer('MetricsClient', 'package:orm/orm.dart'); + builder.type = refer('Engine?', 'package:orm/orm.dart'); }); diff --git a/packages/orm/bin/src/generator+client.dart b/packages/orm/bin/src/generator+client.dart.without_code_builder_test.dart similarity index 100% rename from packages/orm/bin/src/generator+client.dart rename to packages/orm/bin/src/generator+client.dart.without_code_builder_test.dart diff --git a/packages/orm/lib/src/datasources/datasources.dart b/packages/orm/lib/src/datasources/datasources.dart index 6a07ecaf..fb4c5a66 100644 --- a/packages/orm/lib/src/datasources/datasources.dart +++ b/packages/orm/lib/src/datasources/datasources.dart @@ -1,4 +1,4 @@ -enum DatasourceType { url, enviroment } +enum DatasourceType { url, environment } class Datasource { final DatasourceType type; diff --git a/packages/orm/lib/src/engines/binary_engine.dart b/packages/orm/lib/src/engines/binary_engine.dart index 045ff35f..f2d7b1d1 100644 --- a/packages/orm/lib/src/engines/binary_engine.dart +++ b/packages/orm/lib/src/engines/binary_engine.dart @@ -204,7 +204,7 @@ extension on BinaryEngine { this.datasources.map((name, datasource) { final url = switch (datasource) { Datasource(type: DatasourceType.url, value: final url) => url, - Datasource(type: DatasourceType.enviroment, value: final name) => + Datasource(type: DatasourceType.environment, value: final name) => Prisma.env(name).or( () => throw PrismaClientInitializationError( errorCode: "P1013", From b1ec5dde7b4fa55dee0240cae0ebf3b4098138f7 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Thu, 6 Jun 2024 23:49:24 +0800 Subject: [PATCH 11/20] refactor: throws prisma client errors --- packages/orm/bin/src/generate_client.dart | 61 +++++++++++++++---- packages/orm/bin/src/generate_delegate.dart | 2 +- packages/orm/lib/src/base_prisma_client.dart | 12 +++- .../orm/lib/src/engines/binary_engine.dart | 49 +++++++++++++-- .../orm/lib/src/prisma_client_options.dart | 4 +- .../src/runtime/json_protocol/serialize.dart | 7 +-- .../orm/lib/src/runtime/raw/raw_client.dart | 28 ++++----- .../transaction/transaction_client.dart | 20 +++--- 8 files changed, 131 insertions(+), 52 deletions(-) diff --git a/packages/orm/bin/src/generate_client.dart b/packages/orm/bin/src/generate_client.dart index 9db4d238..1ece2f46 100644 --- a/packages/orm/bin/src/generate_client.dart +++ b/packages/orm/bin/src/generate_client.dart @@ -6,39 +6,78 @@ import 'package:orm/generator_helper.dart' as gh; import 'generate_delegate.dart'; import 'generator.dart'; -import 'utils/is_flutter_engine_type.dart'; import 'utils/dart_style_fixer.dart'; -import 'utils/reference.dart'; extension GenerateClient on Generator { Class generateClient() { return Class((builder) { builder.name = 'PrismaClient'; - builder.extend = refer('BasePrismaClient', 'package:orm/orm.dart'); + builder.extend = TypeReference((builder) { + builder.symbol = 'BasePrismaClient'; + builder.url = 'package:orm/orm.dart'; + builder.types.add(refer('PrismaClient')); + }); + + builder.constructors.add(_defaultConstructor); builder.fields.add(generateDatamodel()); builder.fields.add(_engineField); + builder.fields.add(Field((builder) { + builder.name = '_transaction'; + builder.type = + refer('TransactionClient?', 'package:orm/orm.dart'); + })); - builder.constructors.add(_defaultConstructor); + builder.methods.add(createTransactionGetter()); builder.methods.add(createEngineGetter()); + builder.methods.add(Method((getter) { + getter.name = '\$datamodel'; + getter.annotations.add(refer('override')); + getter.lambda = true; + getter.type = MethodType.getter; + getter.body = Code('datamodel'); + })); for (final mapping in options.dmmf.mappings.modelOperations) { final method = generateModelOperation(mapping); builder.methods.add(method); } - - final allowRaw = options.dmmf.mappings.otherOperations.write - .contains('queryRaw') && - options.dmmf.mappings.otherOperations.write.contains('executeRaw'); - if (allowRaw) { - // builder.methods.add(_rawClientGetter); - } }); } } extension on Generator { + Method createTransactionGetter() { + return Method((getter) { + getter.type = MethodType.getter; + getter.name = '\$transaction'; + getter.annotations.add(refer('override')); + getter.body = Block.of([ + Code('if (_transaction != null) return _transaction!;'), + Code('PrismaClient factory('), + refer('TransactionClient', 'package:orm/orm.dart').code, + Code('transaction) {'), + Code(''' + final client = PrismaClient( + datasources: \$options.datasources, + datasourceUrl: \$options.datasourceUrl, + errorFormat: \$options.errorFormat, + log: \$options.logEmitter.definition, + ); + client.\$options.logEmitter = \$options.logEmitter; + client._transaction = transaction; + + return client; + '''), + Code('}'), + Code('return _transaction ='), + refer('TransactionClient', 'package:orm/orm.dart').code, + Code('(\$engine, factory);'), + ]); + }); + } + Method createEngineGetter() { final engine = switch (options.generator.config['engineType']) { 'flutter' => diff --git a/packages/orm/bin/src/generate_delegate.dart b/packages/orm/bin/src/generate_delegate.dart index e1303e40..3f5ea9d3 100644 --- a/packages/orm/bin/src/generate_delegate.dart +++ b/packages/orm/bin/src/generate_delegate.dart @@ -147,7 +147,7 @@ extension on Generator { Code generateResult() { final request = - refer('_client').property('_engine').property('request').call([ + refer('_client').property('\$engine').property('request').call([ refer('query') ], { 'headers': refer('_client').property('\$transaction').property('headers'), diff --git a/packages/orm/lib/src/base_prisma_client.dart b/packages/orm/lib/src/base_prisma_client.dart index f1fa77c0..4a4d0fd5 100644 --- a/packages/orm/lib/src/base_prisma_client.dart +++ b/packages/orm/lib/src/base_prisma_client.dart @@ -1,9 +1,13 @@ +import 'dmmf/datamodel.dart' show DataModel; import 'logging.dart'; import 'prisma_client_options.dart'; import 'runtime/engine.dart'; +import 'runtime/metrics/metrics_client.dart'; +import 'runtime/raw/raw_client.dart'; +import 'runtime/transaction/transaction_client.dart'; /// Base prisma client -abstract class BasePrismaClient { +abstract class BasePrismaClient> { BasePrismaClient({ String? datasourceUrl, final Map? datasources, @@ -22,7 +26,11 @@ abstract class BasePrismaClient { PrismaClientOptions get $options => _options; /// Returns the [Engine] instance typeof [E]. - E get $engine; + Engine get $engine; + RawClient get $raw => RawClient(this as Client); + MetricsClient get $metrics => MetricsClient($engine); + TransactionClient get $transaction; + DataModel get $datamodel; Future $connect() => $engine.start(); Future $disconnect() => $engine.stop(); diff --git a/packages/orm/lib/src/engines/binary_engine.dart b/packages/orm/lib/src/engines/binary_engine.dart index f2d7b1d1..86fb86b8 100644 --- a/packages/orm/lib/src/engines/binary_engine.dart +++ b/packages/orm/lib/src/engines/binary_engine.dart @@ -71,8 +71,10 @@ class BinaryEngine extends Engine { return switch (result) { {'data': final Map data} => deserializeJsonResponse(data), - {'errors': final Iterable errors} => throw Exception(errors), - _ => throw Exception(result), + {'errors': final Iterable errors} => throwErrors(errors), + _ => throw PrismaClientUnknownRequestError( + message: json.encode(PrismaClientUnknownRequestError), + ), }; } @@ -91,7 +93,7 @@ class BinaryEngine extends Engine { final result = await response.json(); return switch (result) { - {'errors': final Iterable errors} => throw Exception(errors), + {'errors': final Iterable errors} => throwErrors(errors), _ => null, }; } @@ -139,8 +141,10 @@ class BinaryEngine extends Engine { return switch (result) { {'id': final String id} => Transaction(id), - {'errors': final Iterable errors} => throw Exception(errors), - _ => throw Exception(result), + {'errors': final Iterable errors} => throwErrors(errors), + _ => throw PrismaClientUnknownRequestError( + message: json.encode(PrismaClientUnknownRequestError), + ), }; } @@ -423,6 +427,41 @@ extension on BinaryEngine { return null; } } + + Never throwErrors(Iterable errors) { + if (errors.length == 1) { + throw switch (errors.single) { + Map payload => throwPrismaKnowError(payload), + Object message => + throw PrismaClientUnknownRequestError(message: json.encode(message)), + _ => + throw PrismaClientUnknownRequestError(message: json.encode(errors)), + }; + } + + throw PrismaClientUnknownRequestError(message: json.encode(errors)); + } + + Never throwPrismaKnowError(Map payload) { + final userFacingError = payload['user_facing_error']; + + if (userFacingError + case { + 'error_code': final String errorCode, + 'message': final String message + }) { + throw PrismaClientKnownRequestError( + code: errorCode, + message: message, + meta: switch (userFacingError['meta']) { + Map meta => meta.map((k, v) => MapEntry(k.toString(), v)), + _ => null, + }, + ); + } + + throw PrismaClientUnknownRequestError(message: payload['error']!); + } } extension on T? { diff --git a/packages/orm/lib/src/prisma_client_options.dart b/packages/orm/lib/src/prisma_client_options.dart index a1f1fa63..36f91853 100644 --- a/packages/orm/lib/src/prisma_client_options.dart +++ b/packages/orm/lib/src/prisma_client_options.dart @@ -30,10 +30,10 @@ class PrismaClientOptions { final ErrorFormat errorFormat; /// Log emitter. - final LogEmitter logEmitter; + LogEmitter logEmitter; /// Create Prisma client options - const PrismaClientOptions({ + PrismaClientOptions({ this.datasourceUrl, this.datasources, required this.errorFormat, diff --git a/packages/orm/lib/src/runtime/json_protocol/serialize.dart b/packages/orm/lib/src/runtime/json_protocol/serialize.dart index 3eae0728..aac14487 100644 --- a/packages/orm/lib/src/runtime/json_protocol/serialize.dart +++ b/packages/orm/lib/src/runtime/json_protocol/serialize.dart @@ -3,6 +3,7 @@ import 'dart:collection'; import 'dart:typed_data'; import '../../../../dmmf.dart' as dmmf; +import '../../errors.dart'; import '../json_convertible.dart'; import '../prisma_enum.dart'; import '../prisma_json.dart'; @@ -88,11 +89,7 @@ class _Context { } Never throwValidationError(String message) { - throw ArgumentError.value( - message, - 'args', - 'Invalid arguments for `${action.toApiFunctionName()}` action on `$modelName` model', - ); + throw PrismaClientValidationError(message: message); } dmmf.Model? get model => datamodel.models diff --git a/packages/orm/lib/src/runtime/raw/raw_client.dart b/packages/orm/lib/src/runtime/raw/raw_client.dart index 78f5e163..884372c0 100644 --- a/packages/orm/lib/src/runtime/raw/raw_client.dart +++ b/packages/orm/lib/src/runtime/raw/raw_client.dart @@ -1,23 +1,14 @@ import 'dart:convert'; import '../../base_prisma_client.dart'; -import '../../dmmf/datamodel.dart' show DataModel; -import '../engine.dart'; import '../json_protocol/protocol.dart'; import '../json_protocol/serialize.dart'; -import '../transaction/transaction_client.dart'; import '_internal/raw_parameter_codec.dart'; -class RawClient { - final Engine _engine; - final DataModel _datamodel; - final TransactionClient _client; +class RawClient> { + final Client _client; - const RawClient( - Engine engine, DataModel datamodel, TransactionClient transaction) - : _engine = engine, - _datamodel = datamodel, - _client = transaction; + const RawClient(Client client) : _client = client; Future query(String sql, [Iterable? parameters]) => _inner( action: JsonQueryAction.queryRaw, @@ -42,13 +33,16 @@ class RawClient { 'parameters': json.encode(rawParameter.encode(parameters ?? const [])), }; - final query = - serializeJsonQuery(datamodel: _datamodel, action: action, args: args); + final query = serializeJsonQuery( + datamodel: _client.$datamodel, + action: action, + args: args, + ); - final result = await _engine.request( + final result = await _client.$engine.request( query, - headers: _client.headers, - transaction: _client.transaction, + headers: _client.$transaction.headers, + transaction: _client.$transaction.transaction, ); return rawParameter.decode(result[action.name]); diff --git a/packages/orm/lib/src/runtime/transaction/transaction_client.dart b/packages/orm/lib/src/runtime/transaction/transaction_client.dart index a663ac5b..b2384f1d 100644 --- a/packages/orm/lib/src/runtime/transaction/transaction_client.dart +++ b/packages/orm/lib/src/runtime/transaction/transaction_client.dart @@ -1,18 +1,19 @@ import '../../base_prisma_client.dart'; +import '../../errors.dart'; import '../engine.dart'; import 'isolation_level.dart'; import 'transaction_headers.dart'; import 'transaction.dart'; -class TransactionClient { +class TransactionClient> { final Engine _engine; - final T Function(TransactionClient transactionClient) _factory; + final Client Function(TransactionClient transactionClient) _factory; final TransactionHeaders? headers; final Transaction? transaction; const TransactionClient._({ - required T Function(TransactionClient) factory, + required Client Function(TransactionClient) factory, required Engine engine, required this.headers, required this.transaction, @@ -20,14 +21,14 @@ class TransactionClient { _engine = engine; const TransactionClient( - Engine engine, T Function(TransactionClient) factory) + Engine engine, Client Function(TransactionClient) factory) : _factory = factory, _engine = engine, headers = null, transaction = null; /// Start a new transaction. - Future start({ + Future start({ TransactionHeaders? headers, int maxWait = 2000, int timeout = 5000, @@ -59,7 +60,8 @@ class TransactionClient { /// Commit a transaction. Future commit() { if (headers == null || transaction == null) { - throw Exception('Cannot commit a transaction that has not been started.'); + throw PrismaClientValidationError( + message: 'Cannot commit a transaction that has not been started.'); } return _engine.commitTransaction( @@ -71,8 +73,8 @@ class TransactionClient { /// Rollback a transaction. Future rollback() { if (headers == null || transaction == null) { - throw Exception( - 'Cannot rollback a transaction that has not been started.'); + throw PrismaClientValidationError( + message: 'Cannot rollback a transaction that has not been started.'); } return _engine.rollbackTransaction( @@ -92,7 +94,7 @@ class TransactionClient { /// }); /// ``` Future call( - Future Function(T tx) fn, { + Future Function(Client prisma) fn, { int maxWait = 2000, int timeout = 5000, TransactionIsolationLevel? isolationLevel, From 3aa26340b2caf056e4599d4c8362ec3f64e329b5 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Thu, 6 Jun 2024 23:56:20 +0800 Subject: [PATCH 12/20] chore: * --- packages/orm/CHANGELOG.md | 474 +--------------------------------- packages/orm/lib/version.dart | 2 +- packages/orm/pubspec.yaml | 2 +- 3 files changed, 8 insertions(+), 470 deletions(-) diff --git a/packages/orm/CHANGELOG.md b/packages/orm/CHANGELOG.md index 985ec4b0..f926a17c 100644 --- a/packages/orm/CHANGELOG.md +++ b/packages/orm/CHANGELOG.md @@ -1,482 +1,20 @@ -# Prisma Dart v4.4.0 +# Prisma Dart v5.0.0 -To install Prisma ORM for Dart v4.4.0 run this command: +To install Prisma ORM for Dart v5.0.0 run this command: ```bash -dart pub add orm:4.4.0 +dart pub add orm:^5.0.0 ``` or update your `pubspec.yaml` file: ```yaml dependencies: - orm: ^4.4.0 + orm: ^5.0.0 ``` ## What's Changed -* fix(deps): update dependency webfetch to ^0.0.17 by @renovate in https://github.com/medz/prisma-dart/pull/383 -* feat(client): support `createManyAndReturn` model delegate method by @medz in https://github.com/medz/prisma-dart/pull/386 + -**Full Changelog**: https://github.com/medz/prisma-dart/compare/orm-v4.3.0...orm-v4.4.0 - -# Prisma Dart v4.3.0 - -To install Prisma ORM for Dart v4.3.0 run this command: - -```bash -dart pub add orm:4.3.0 -``` - -or update your `pubspec.yaml` file: - -```yaml -dependencies: - orm: ^4.3.0 -``` - -## What's Changed - -* fix(deps): update dependency decimal to v3 - -# Prisma Dart v4.2.1 - -To install Prisma ORM for Dart v4.2.1 run this command: - -```bash -dart pub add orm:4.2.1 -``` - -or update your `pubspec.yaml` file: - -```yaml -dependencies: - orm: ^4.2.1 -``` - -## What's Changed - -* chore(deps): update dependency ffigen to v12 by @renovate in https://github.com/medz/prisma-dart/pull/368 -* Support flutter android by @medz in https://github.com/medz/prisma-dart/pull/377 -* fix: Update null fields fail by @medz in https://github.com/medz/prisma-dart/pull/381 -* fix(orm): Fix incorrect definition of `toJson` to `Object?` type. by @medz in https://github.com/medz/prisma-dart/pull/380 - - -**Full Changelog**: https://github.com/medz/prisma-dart/compare/orm-v4.2.0...orm-v4.2.1 - -# Prisma Dart v4.2.0 - -To install Prisma Client for Dart v4.2.0 run: - -```bash -dart pub add orm:4.2.0 -``` - -Or update your `pubspec.yaml` file: - -```yaml -dependencies: - orm: ^4.2.0 -``` - -## What's Changed - -* feat(orm): Add `PrismaClient.use` factory. -* chore(deps): update dependency `flutter_lints` to v4 by @renovate in https://github.com/medz/prisma-dart/pull/369 - -# Prisma Dart v4.1.1 - -To install Prisma Client for Dart v4.1.1 run: - -```bash -dart pub add orm:4.1.1 -``` - -Or update your `pubspec.yaml` file: - -```yaml -dependencies: - orm: ^4.1.1 -``` - -## What's Changed - -* Fixed typo in Changelog | Priama -> Prisma by @saphalpdyl in https://github.com/medz/prisma-dart/pull/358 -* fix(deps): update dependency webfetch to ^0.0.16 by @renovate in https://github.com/medz/prisma-dart/pull/359 -* chore(deps): update dependency lints to v4 by @renovate in https://github.com/medz/prisma-dart/pull/362 -* feat: Added docs of running on Docker by @kerimamansaryyev in https://github.com/medz/prisma-dart/pull/363 -* fixes: #365; by @atharvapalaskar in https://github.com/medz/prisma-dart/pull/366 - -## New Contributors - -* @saphalpdyl made their first contribution in https://github.com/medz/prisma-dart/pull/358 -* @renovate made their first contribution in https://github.com/medz/prisma-dart/pull/359 -* @kerimamansaryyev made their first contribution in https://github.com/medz/prisma-dart/pull/363 -* @atharvapalaskar made their first contribution in https://github.com/medz/prisma-dart/pull/366 - -**Full Changelog**: https://github.com/medz/prisma-dart/compare/v4.1.0...orm-v4.1.1 - -# Prisma Client Dart v4.1.0 - -To install Prisma Client for Dart v4.1.0 run: - -```bash -dart pub add orm:4.1.0 -``` - -Or update your `pubspec.yaml` file: - -```yaml -dependencies: - orm: 4.1.0 -``` - -## What's Changed - -- data model add `toJson` method by @medz in https://github.com/medz/prisma-dart/pull/352 - -# Prisma Client Dart v4.0.3 - -To install Prisma Client for Dart v4.0.3 run: - -```bash -dart pub add orm:4.0.3 -``` - -Or update your `pubspec.yaml` file: - -```yaml -dependencies: - orm: 4.0.3 -``` - -## What's Changed - -- **Bug**: fix for `code_builder` not handling CRLF case - -# Prisma Client Dart v4.0.2 - -To install Prisma Client for Dart v4.0.2 run: - -```bash -dart pub add orm:4.0.2 -``` - -Or update your `pubspec.yaml` file: - -```yaml -dependencies: - orm: 4.0.2 -``` - -## What's Changed - -- **Bug**: Fixed the `Json` class not found in the generated code. - -# Prisma Client Dart v4.0.1 - -To install Prisma Client for Dart v4.0.1 run: - -```bash -dart pub add orm:4.0.1 -``` - -Or update your `pubspec.yaml` file: - -```yaml -dependencies: - orm: 4.0.1 -``` - -## What's Changed - -- **Bug**(Generator): Fixed duplicate enum field name is `name`. - -# Prisma Client Dart v4.0.0 - -To install Prisma Client Dart v4.0.0 run: - -```bash -dart pub add orm:4.0.0 -``` - -Or update your `pubspec.yaml` file: - -```yaml -dependencies: - orm: 4.0.0 -``` - -## What's Changed - -- **Bug**: Fixed list of primitive type not parsing correctly (https://github.com/medz/prisma-dart/issue/331) - -# Prisma Client Dart v4.0.0-beta.3 - -To install Prisma Client Dart v4.0.0-beta.3 run: - -```bash -dart pub add orm:4.0.0-beta.3 -``` - -Or update your `pubspec.yaml` file: - -```yaml -dependencies: - orm: 4.0.0-beta.3 -``` - -## What's Changed - -- **Bug**: Fixed list of enum not parsing correctly (https://github.com/medz/prisma-dart/issues/329) - -## Credits - -- @lucasvaudey -- @NeroSong - -# Prisma Client Dart v4.0.0-beta.2 - -To install Prisma Client Dart v4.0.0-beta.2 run: - -```bash -dart pub add orm:4.0.0-beta.2 -``` - -Or update your `pubspec.yaml` file: - -```yaml -dependencies: - orm: 4.0.0-beta.2 -``` - -## What's Changed - -1. **Bug**: Use relative paths to execute the engine. - -# Prisma Client Dart v4.0.0-beta.1 - -To install Prisma Client Dart v4.0.0-beta.1 run: - -```bash -dart pub add orm:4.0.0-beta.1 -``` - -Or update your `pubspec.yaml` file: - -```yaml -dependencies: - orm: 4.0.0-beta.1 -``` - -## What's Changed - -- fix relations are throwing error when you use include, https://github.com/medz/prisma-dart/issues/324 - -## Thanks - -@kidusdev - -# Prisma Client Dart v4.0.0-beta - -To install Prisma Client Dart v4.0.0-beta run: - -```bash -dart pub add orm:4.0.0-beta -``` - -Or update your `pubspec.yaml` file: - -```yaml -dependencies: - orm: 4.0.0-beta -``` - -## What's Changed - -The **first Beta version is released**. This version is a major version. We will conduct a lot of testing on this version to ensure the stability of this version. -In addition, there are no actual code updates in this version, the documentation brings updates to the API Reference: - -1. [Client API Reference](https://prisma.pub/references/client-api.html) -2. [Model Delegate API Reference](https://prisma.pub/references/model-delegate.html) - -# Prisma Dart client v4.0.0-alpha.5 - -To install Prisma Dart client v4.0.0-alpha.5 run: - -```sh -dart pub add orm:4.0.0-alpha.5 -``` - -Or update your `pubspec.yaml` file: - -```yaml -dependencies: - orm: 4.0.0-alpha.5 -``` - -## What's Changed - -1. **FIX** fix transaction isolation level not expected. - -# Prisma Dart client v4.0.0-alpha.4 - -To install Prisma Dart client v4.0.0-alpha.4 run: - -```sh -dart pub add orm:4.0.0-alpha.4 -``` - -Or update your `pubspec.yaml` file: - -```yaml -dependencies: - orm: 4.0.0-alpha.4 -``` - -## What's Changed - -1. **OPTIMIZATION** Prisma Dart client generator outputs more user-friendly error messages that help you know where the problem lies. -2. **REFACTOR** Adjust src code storage structure. -3. **FIX** Rethrow start error -4. **FIX** Fix input types required multiple fields. -5. **FIX** Fix relation count not arguments in include. - -# Prisma Dart client v4.0.0-alpha.3 - -To install Prisma Dart client v4.0.0-alpha.3 run: - -```sh -dart pub add orm:4.0.0-alpha.3 -``` - -Or update your `pubspec.yaml` file: - -```yaml -dependencies: - orm: 4.0.0-alpha.3 -``` - -## What's Changed - -1. Dump `webfetch` to `0.0.14` version. -2. Fixed using a model name with an underscore (`_`) in `schema.prisma` causing the generator to fail. - -# Prisma Dart client v4.0.0-alpha.2 - -To install Prisma Dart client v4.0.0-alpha.2 run: - -```sh -dart pub add orm:4.0.0-alpha.2 -``` - -Or update your `pubspec.yaml` file: - -```yaml -dependencies: - orm: 4.0.0-alpha.2 -``` - -## What's Changed - -1. Change Dart SDK version to `^3.2.0` -2. Support **RAW** query and execute feature, See [Raw queries](#raw-queries) - -## Raw queries - -You can use `$raw` to execute raw queries - -### `$raw.query` - -Execute a raw query, for example: - -```dart -final result = await prisma.$raw.query('SELECT * FROM "User"'); // PostgreSQL -``` - -### `$raw.execute` - -Execute a raw query, for example: - -```dart -final result = await prisma.$raw.execute('DELETE FROM "User"'); // PostgreSQL -``` - -### Parameters - -`$raw.query` and `$raw.execute` support parameters, for example: - -```dart -// PostgreSQL -final result = await prisma.$raw.query( - 'SELECT * FROM "User" WHERE "id" = \$1', - [1], -); - -// MySQL -final result = await prisma.$raw.query( - 'SELECT * FROM `User` WHERE `id` = ?', - [1], -); -``` - -> SQL template string see your used database. - -# Prisma Dart client v4.0.0-alpha.1 - -To install Prisma Dart client v4.0.0-alpha.0 run: - -```sh -dart pub add orm:4.0.0-alpha.1 -``` - -Or update your `pubspec.yaml` file: - -```yaml -dependencies: - orm: 4.0.0-alpha.1 -``` - -## What's Changed - -1. Refactor the entire client generator -2. `PrismaClient` is now generated by the generator -3. Ability to operate transactions on your own -4. Use binary engine by default -5. Supports full select and include features. - -# Prisma Dart client v4.0.0-alpha.0 - -To install Prisma Dart client v4.0.0-alpha.0 run: - -```sh -# If you are using Dart -dart pub add orm:4.0.0-alpha.0 - -# Or if you are using Flutter -flutter pub add orm:4.0.0-alpha.0 -``` - -To upgrade to Prisma Dart client v4.0.0-alpha.0, Please follow the [announcements](https://github.com/medz/prisma-dart/discussions/292) and update your `pubspec.yaml` file: - -```yaml -dependencies: - orm: 4.0.0-alpha.0 -``` - -[Read Prisma Dart Client v4.0.0-alpha.0 release notes on the Prisma Dart discussions](https://github.com/medz/prisma-dart/discussions/292) - -## What's Changed - -- Whole project refactoring -- Remove any JSON serialization tool, now it's ready to use by just generating the client without any other dependencies and extra commands -- Switch from GraphQL protocol to JSON protocol -- Client takes a standalone Prisma engine instance -- Client and all input/output types are standalone and can be distributed to any Dart platform -- Add database field reference support -- Support `select` feature (incomplete, currently only support rough one-level Model fields) -- Support `include` feature (incomplete, currently only support rough one-level Model fields) -- `PrismaUnion` regression, now can structure nested inputs of multiple parameters via union -- `PrismaNull` regression, now support database `null` data setting -- DMMF, generator helpers regression, no need to depend on other packages, can directly use `orm` as the base package for developing Dart Prisma ecosystem packages -- Add `Decimal` type support (from [decimal package](https://pub.dev/package/decimal), exported by `orm` proxy) +**Full Changelog**: https://github.com/medz/prisma-dart/compare/orm-v4.4.0...orm-v5.0.0 diff --git a/packages/orm/lib/version.dart b/packages/orm/lib/version.dart index 58d44db5..05133278 100644 --- a/packages/orm/lib/version.dart +++ b/packages/orm/lib/version.dart @@ -1,3 +1,3 @@ library prisma.version; -const version = '4.4.0'; +const version = '5.0.0'; diff --git a/packages/orm/pubspec.yaml b/packages/orm/pubspec.yaml index 4f9f7852..960a16b5 100644 --- a/packages/orm/pubspec.yaml +++ b/packages/orm/pubspec.yaml @@ -1,6 +1,6 @@ name: orm description: Next-generation ORM for Dart & Flutter | PostgreSQL, MySQL, MariaDB, SQL Server, SQLite, MongoDB and CockroachDB. -version: 4.4.0 +version: 5.0.0 homepage: https://prisma.pub repository: https://github.com/medz/prisma-dart funding: From 5d7f0da103800db24c55e89f228fb24f9124c2e7 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Fri, 7 Jun 2024 00:00:32 +0800 Subject: [PATCH 13/20] Update CHANGELOG.md --- packages/orm/CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/orm/CHANGELOG.md b/packages/orm/CHANGELOG.md index f926a17c..855ce36d 100644 --- a/packages/orm/CHANGELOG.md +++ b/packages/orm/CHANGELOG.md @@ -15,6 +15,10 @@ dependencies: ## What's Changed - +* refactor: throws prisma client errors +* refactor(engine): Refactoring binary engines +* refactor(client): add base client and client options +* feat(client): add `env` support +* feat(client): add logging **Full Changelog**: https://github.com/medz/prisma-dart/compare/orm-v4.4.0...orm-v5.0.0 From 6d190b990e0af4542104b8d24d5cca74424141d5 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Fri, 7 Jun 2024 01:54:37 +0800 Subject: [PATCH 14/20] chore: remove retry package --- examples/with_sqlite/.env | 3 +- examples/with_sqlite/bin/with_sqlite.dart | 6 +- examples/with_sqlite/dev.db | 0 examples/with_sqlite/prisma/schema.prisma | 7 +- examples/with_sqlite/pubspec.lock | 42 +++++++---- packages/orm/bin/orm.dart | 2 +- packages/orm/lib/engines/binary.dart | 1 - .../_validate_datasource_url.io.dart | 8 +- .../orm/lib/src/engines/binary_engine.dart | 75 ++++++++++++++----- packages/orm/pubspec.yaml | 3 +- 10 files changed, 100 insertions(+), 47 deletions(-) delete mode 100644 examples/with_sqlite/dev.db diff --git a/examples/with_sqlite/.env b/examples/with_sqlite/.env index c498ab59..cbd9cf24 100644 --- a/examples/with_sqlite/.env +++ b/examples/with_sqlite/.env @@ -4,4 +4,5 @@ # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. # See the documentation for all the connection string options: https://pris.ly/d/connection-strings -DATABASE_URL="file:./dev.db" \ No newline at end of file +DATABASE_URL="file:./prisma/dev.db" # Dart rintime using +DIRECT_DATABASE_URL="file:./dev.db" # Prisma CLI using diff --git a/examples/with_sqlite/bin/with_sqlite.dart b/examples/with_sqlite/bin/with_sqlite.dart index c9697c38..a56abcc4 100644 --- a/examples/with_sqlite/bin/with_sqlite.dart +++ b/examples/with_sqlite/bin/with_sqlite.dart @@ -1,10 +1,6 @@ -import 'dart:io'; - import 'package:with_sqlite/with_sqlite.dart'; -final datasourceUrl = - 'file:${File.fromUri(Platform.script).parent.parent.path}/prisma/dev.db'; -final prisma = PrismaClient(datasourceUrl: datasourceUrl); +final prisma = PrismaClient(); Future main() async { try { diff --git a/examples/with_sqlite/dev.db b/examples/with_sqlite/dev.db deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/with_sqlite/prisma/schema.prisma b/examples/with_sqlite/prisma/schema.prisma index 6fcf9280..2878dee0 100644 --- a/examples/with_sqlite/prisma/schema.prisma +++ b/examples/with_sqlite/prisma/schema.prisma @@ -7,8 +7,11 @@ generator client { } datasource db { - provider = "sqlite" - url = env("DATABASE_URL") + provider = "sqlite" + url = env("DATABASE_URL") + // Why need directUrl ? + // The Prisma CLI tool uses the file URL relative to the prisma directory when it is configured at runtime. The Dart runtime uses PWD + directUrl = env("DIRECT_DATABASE_URL") } model Game { diff --git a/examples/with_sqlite/pubspec.lock b/examples/with_sqlite/pubspec.lock index 46364d89..b2d9d927 100644 --- a/examples/with_sqlite/pubspec.lock +++ b/examples/with_sqlite/pubspec.lock @@ -62,6 +62,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.9.2" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" code_builder: dependency: transitive description: @@ -106,10 +114,10 @@ packages: dependency: transitive description: name: decimal - sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21" + sha256: d40902905d9b1347cdd6cfb7c144025557cd49d12725a87f9177df93abd0d825 url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "3.0.1" file: dependency: transitive description: @@ -134,6 +142,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + intl: + dependency: transitive + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" json_rpc_2: dependency: transitive description: @@ -196,7 +212,7 @@ packages: path: "../../packages/orm" relative: true source: path - version: "4.2.0" + version: "5.0.0" package_config: dependency: transitive description: @@ -229,22 +245,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.3" - recase: + rc: dependency: transitive description: - name: recase - sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + name: rc + sha256: "0c2d562c5925b68687ae86b387d847cfc175a5d1d2fce2956a9acc461bd2f33e" url: "https://pub.dev" source: hosted - version: "4.1.0" - retry: + version: "0.3.3" + recase: dependency: transitive description: - name: retry - sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "4.1.0" source_span: dependency: transitive description: @@ -321,10 +337,10 @@ packages: dependency: transitive description: name: webfetch - sha256: "42775e831b0d8af643a355e71a39adad2ca76ea9f73b06de47be075c9f45f90a" + sha256: ad651c7ab3829c653732facf3eefa03ef6e023575ee8172b8b60bbdf117cdfd9 url: "https://pub.dev" source: hosted - version: "0.0.16" + version: "0.0.17" yaml: dependency: transitive description: diff --git a/packages/orm/bin/orm.dart b/packages/orm/bin/orm.dart index e4ecb5e1..d3211e91 100644 --- a/packages/orm/bin/orm.dart +++ b/packages/orm/bin/orm.dart @@ -54,7 +54,7 @@ Future generate(GeneratorOptions options) async { await output.writeAsString(formated); } - if (isFlutterEngineType(options.generator.config)) { + if (!isFlutterEngineType(options.generator.config)) { // Copy prisma query engine. final engineDownloadPath = options.binaryPaths.queryEngine?.values.firstOrNull; diff --git a/packages/orm/lib/engines/binary.dart b/packages/orm/lib/engines/binary.dart index 40e0a9a8..6e0c2597 100644 --- a/packages/orm/lib/engines/binary.dart +++ b/packages/orm/lib/engines/binary.dart @@ -1,4 +1,3 @@ library prisma.engines.binary; - export '../src/engines/binary_engine.dart'; diff --git a/packages/orm/lib/src/datasources/_validate_datasource_url.io.dart b/packages/orm/lib/src/datasources/_validate_datasource_url.io.dart index 3616d17a..8fea1a6b 100644 --- a/packages/orm/lib/src/datasources/_validate_datasource_url.io.dart +++ b/packages/orm/lib/src/datasources/_validate_datasource_url.io.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:path/path.dart' as path; +import '../_internal/project_directory.dart'; import '../errors.dart'; import '_validate_datasource_url.dart' as shared; @@ -11,9 +12,10 @@ String validateDatasourceURL(String datasourceUrl, {bool isPorxy = false}) { return shared.validateDatasourceURL(datasourceUrl, isPorxy: isPorxy); } - final databaseFile = File.fromUri(url); - final databasePath = path.relative(databaseFile.path); - if (databaseFile.existsSync()) { + final pwd = findProjectDirectory()?.path ?? Directory.current.path; + final databasePath = + path.relative(path.join(pwd, datasourceUrl.substring(5))); + if (File(databasePath).existsSync()) { return 'file:$databasePath'; } diff --git a/packages/orm/lib/src/engines/binary_engine.dart b/packages/orm/lib/src/engines/binary_engine.dart index 86fb86b8..1eeae586 100644 --- a/packages/orm/lib/src/engines/binary_engine.dart +++ b/packages/orm/lib/src/engines/binary_engine.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:path/path.dart' as path; -import 'package:retry/retry.dart'; import 'package:webfetch/webfetch.dart' show fetch; import '../_internal/project_directory.dart'; @@ -189,7 +188,7 @@ extension on BinaryEngine { final enginePaths = generateEnginePaths([ Directory.current, if (projectDirectory != null) projectDirectory, - ]); + ]).toSet(); for (final enginePath in enginePaths) { final file = File(enginePath); @@ -199,7 +198,7 @@ extension on BinaryEngine { throw PrismaClientInitializationError( errorCode: "QE404", message: - 'No binary engine found, please make sure any of the following locations contain the executable file: $enginePaths'); + 'No binary engine found, please make sure any of the following locations contain the executable file: ${enginePaths.map((e) => path.relative(e)).toSet()}'); } /// Creates overwrite datasources string. @@ -306,10 +305,11 @@ extension on BinaryEngine { } }); - final endpointCompleter = Completer.sync(); + Uri? endpoint; final stdoutSubscription = process.stdout.byline().listen((event) { + print(event); final payload = tryParseJSON(event); - tryCompleteEndpoint(endpointCompleter, payload); + tryCompleteEndpoint(payload, (uri) => endpoint = uri); if (payload case {'span': true, 'spans': final List _}) { // TODO: Tracing engine span. current print payload to console. @@ -336,18 +336,55 @@ extension on BinaryEngine { await stdoutSubscription.cancel(); } - final endpoint = await retry(() async { - return endpointCompleter.future.timeout(Duration(seconds: 1), - onTimeout: () { + for (int count = 0;; count++) { + options.logEmitter.emit( + LogLevel.info, + LogEvent( + timestamp: DateTime.now(), + target: 'prisma:client', + message: + 'Whether the engine has started for the ${count + 1} th time.', + ), + ); + + if (endpoint != null) break; + if (count >= 10) { + await stop(); throw PrismaClientInitializationError( - message: 'Prisma binary query engine not ready'); - }); - }).catchError((e) { - stop(); - throw e; - }); + message: 'Engine startup failed.'); + } + + await Future.delayed(Duration(milliseconds: 300 * count)); + } + + for (int count = 0;; count++) { + options.logEmitter.emit( + LogLevel.info, + LogEvent( + timestamp: DateTime.now(), + target: 'prisma:client', + message: + 'Whether the engine has ready for the ${count + 1} th time.'), + ); - return (endpoint, stop); + try { + final response = await fetch(endpoint!.replace(path: '/status')); + final result = await response.json(); + if (result case {"status": "ok"}) { + break; + } + } catch (_) {} + + if (count >= 10) { + await stop(); + throw PrismaClientInitializationError( + message: 'Engine startup failed.'); + } + + await Future.delayed(Duration(milliseconds: 300 * count)); + } + + return (endpoint!, stop); } (LogLevel, EngineEvent) createEngineEvent(payload) { @@ -399,7 +436,7 @@ extension on BinaryEngine { ); } - void tryCompleteEndpoint(Completer completer, payload) { + void tryCompleteEndpoint(payload, void Function(Uri) setter) { if (payload case { 'level': 'INFO', @@ -412,10 +449,10 @@ extension on BinaryEngine { } when message.startsWith('Started query engine http server') && ip.isNotEmpty && - port.isNotEmpty && - !completer.isCompleted) { + port.isNotEmpty) { final endpoint = Uri.http('$ip:$port'); - completer.complete(endpoint); + + setter(endpoint); } } diff --git a/packages/orm/pubspec.yaml b/packages/orm/pubspec.yaml index 960a16b5..ae5246e0 100644 --- a/packages/orm/pubspec.yaml +++ b/packages/orm/pubspec.yaml @@ -20,9 +20,8 @@ dependencies: json_rpc_2: ^3.0.2 logging: ^1.2.0 path: ^1.8.3 - rc: ^0.3.2 + rc: ^0.3.3 recase: ^4.1.0 - retry: ^3.1.2 stream_channel: ^2.1.2 webfetch: ^0.0.17 From 5f698114cd0f0b16b7f451e29fdb821808205336 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Fri, 7 Jun 2024 02:01:24 +0800 Subject: [PATCH 15/20] chore: * --- .github/dependabot.yml | 14 ++++++++++++++ packages/orm_flutter/pubspec.yaml | 4 ++++ 2 files changed, 18 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..1a12c0de --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "pub" + directory: "/packages/orm" + schedule: + interval: "daily" + - package-ecosystem: "pub" + directory: "/packages/orm_flutter" + schedule: + interval: "daily" diff --git a/packages/orm_flutter/pubspec.yaml b/packages/orm_flutter/pubspec.yaml index 32c2744d..02b910b6 100644 --- a/packages/orm_flutter/pubspec.yaml +++ b/packages/orm_flutter/pubspec.yaml @@ -28,6 +28,10 @@ dev_dependencies: sdk: flutter flutter_lints: ^4.0.0 +dependency_overrides: + orm: + path: ../orm + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec From f4aa1c71d539c7c65fd38339c0edd441be17fb08 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Fri, 7 Jun 2024 03:22:51 +0800 Subject: [PATCH 16/20] refactor: Refactoring the Flutter integration engine --- examples/flutter_with_orm/lib/prisma.dart | 16 +-- examples/flutter_with_orm/pubspec.lock | 20 +-- packages/orm/bin/src/generate_client.dart | 2 +- .../orm/lib/src/engines/binary_engine.dart | 26 ++-- packages/orm_flutter/lib/orm_flutter.dart | 2 +- ...lutter_engine.dart => library_engine.dart} | 127 ++++++++++++++---- 6 files changed, 132 insertions(+), 61 deletions(-) rename packages/orm_flutter/lib/src/{prisma_flutter_engine.dart => library_engine.dart} (73%) diff --git a/examples/flutter_with_orm/lib/prisma.dart b/examples/flutter_with_orm/lib/prisma.dart index b8b0717b..a81a41e8 100644 --- a/examples/flutter_with_orm/lib/prisma.dart +++ b/examples/flutter_with_orm/lib/prisma.dart @@ -11,16 +11,14 @@ Future initPrismaClient() async { WidgetsFlutterBinding.ensureInitialized(); final supportDir = await getApplicationSupportDirectory(); - final database = join(supportDir.path, 'database.sqlite'); + final database = join(supportDir.path, 'database.sqlite.db'); - late final PrismaFlutterEngine engine; - prisma = PrismaClient.use((schema, datasources) { - return engine = PrismaFlutterEngine(schema: schema, datasources: { - ...datasources, - 'db': 'file:$database', - }); - }); + prisma = PrismaClient(datasourceUrl: 'file:$database'); + final engine = switch (prisma.$engine) { + LibraryEngine engine => engine, + _ => null, + }; await prisma.$connect(); - await engine.applyMigrations(path: 'prisma/migrations'); + await engine?.applyMigrations(path: 'prisma/migrations'); } diff --git a/examples/flutter_with_orm/pubspec.lock b/examples/flutter_with_orm/pubspec.lock index b5c1b6d6..77bae73c 100644 --- a/examples/flutter_with_orm/pubspec.lock +++ b/examples/flutter_with_orm/pubspec.lock @@ -281,14 +281,14 @@ packages: path: "../../packages/orm" relative: true source: path - version: "4.3.0" + version: "5.0.0" orm_flutter: dependency: "direct main" description: path: "../../packages/orm_flutter" relative: true source: path - version: "0.0.5" + version: "0.0.6" package_config: dependency: transitive description: @@ -385,22 +385,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.2" - recase: + rc: dependency: transitive description: - name: recase - sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + name: rc + sha256: "0c2d562c5925b68687ae86b387d847cfc175a5d1d2fce2956a9acc461bd2f33e" url: "https://pub.dev" source: hosted - version: "4.1.0" - retry: + version: "0.3.3" + recase: dependency: transitive description: - name: retry - sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "4.1.0" sky_engine: dependency: transitive description: flutter diff --git a/packages/orm/bin/src/generate_client.dart b/packages/orm/bin/src/generate_client.dart index 1ece2f46..fae0a0a4 100644 --- a/packages/orm/bin/src/generate_client.dart +++ b/packages/orm/bin/src/generate_client.dart @@ -81,7 +81,7 @@ extension on Generator { Method createEngineGetter() { final engine = switch (options.generator.config['engineType']) { 'flutter' => - refer('PrismaFlutterEngine', 'package:orm_flutter/orm_flutter.dart'), + refer('LibraryEngine', 'package:orm_flutter/orm_flutter.dart'), _ => refer('BinaryEngine', 'package:orm/engines/binary.dart'), }; diff --git a/packages/orm/lib/src/engines/binary_engine.dart b/packages/orm/lib/src/engines/binary_engine.dart index 1eeae586..6cdb817c 100644 --- a/packages/orm/lib/src/engines/binary_engine.dart +++ b/packages/orm/lib/src/engines/binary_engine.dart @@ -205,6 +205,21 @@ extension on BinaryEngine { String createOverwriteDatasourcesString() { Map overwriteDatasources = this.datasources.map((name, datasource) { + if (options.datasourceUrl != null) { + return MapEntry( + name, + Prisma.validateDatasourceURL(options.datasourceUrl!, isPorxy: false), + ); + } + + if (options.datasources?.containsKey(name) == true) { + return MapEntry( + name, + Prisma.validateDatasourceURL(options.datasources![name]!, + isPorxy: false), + ); + } + final url = switch (datasource) { Datasource(type: DatasourceType.url, value: final url) => url, Datasource(type: DatasourceType.environment, value: final name) => @@ -219,17 +234,6 @@ extension on BinaryEngine { return MapEntry(name, Prisma.validateDatasourceURL(url, isPorxy: false)); }); - if (options.datasources?.isNotEmpty == true) { - overwriteDatasources.addAll(options.datasources!); - } - - if (options.datasourceUrl != null) { - final url = - Prisma.validateDatasourceURL(options.datasourceUrl!, isPorxy: false); - overwriteDatasources = - overwriteDatasources.map((name, _) => MapEntry(name, url)); - } - final datasources = overwriteDatasources.entries .map((e) => {'name': e.key, 'url': e.value}) .toList(); diff --git a/packages/orm_flutter/lib/orm_flutter.dart b/packages/orm_flutter/lib/orm_flutter.dart index 3f9c9004..b0933d75 100644 --- a/packages/orm_flutter/lib/orm_flutter.dart +++ b/packages/orm_flutter/lib/orm_flutter.dart @@ -1,3 +1,3 @@ library dev.odroe.prisma.flutter; -export 'src/prisma_flutter_engine.dart'; +export 'src/library_engine.dart' show LibraryEngine; diff --git a/packages/orm_flutter/lib/src/prisma_flutter_engine.dart b/packages/orm_flutter/lib/src/library_engine.dart similarity index 73% rename from packages/orm_flutter/lib/src/prisma_flutter_engine.dart rename to packages/orm_flutter/lib/src/library_engine.dart index 4b5516b7..94c16605 100644 --- a/packages/orm_flutter/lib/src/prisma_flutter_engine.dart +++ b/packages/orm_flutter/lib/src/library_engine.dart @@ -39,30 +39,19 @@ final DynamicLibrary _dylib = () { final _bindingsInstance = QueryEngineBindings(_dylib); int _evalEngineId = 0; -enum LogLevel { error, warn, info, debug, trace } - -class PrismaFlutterEngine extends Engine implements Finalizable { - final Pointer _qe; - - PrismaFlutterEngine._(this._qe, - {required super.schema, required super.datasources}); - - factory PrismaFlutterEngine( - {required String schema, - required Map datasources, - LogLevel logLevel = LogLevel.info, - bool logQueries = false, - void Function(String message)? logCallback, - Map? env, - bool ignoreEnvVarErrors = false, - String nativeConfigDir = ''}) { +class LibraryEngine extends Engine implements Finalizable { + late final Pointer _qe; + bool _started = false; + + LibraryEngine({ + required super.schema, + required super.datasources, + required super.options, + }) { final currentEngindId = _evalEngineId.toString(); void logCallbackProxy(Pointer idptr, Pointer message) { - final id = idptr.cast().toDartString(); - if (id == currentEngindId && logCallback != null) { - logCallback(message.cast().toString()); - } + // TODO: log callback. C-ABI engine bug, unable to parse message } final logNativeCallable = @@ -70,19 +59,32 @@ class PrismaFlutterEngine extends Engine implements Finalizable { logCallbackProxy); final native = Struct.create() - ..config_dir = nativeConfigDir.toNativeUtf8().cast(); + ..config_dir = ''.toNativeUtf8().cast(); + + final logLevel = this.options.logEmitter.definition.fold(null, + (value, current) { + if (current.$1 == LogLevel.query) return value; + if (value == null) return current.$1.name; + if (value == 'info' || current.$1 == LogLevel.info) { + return 'info'; + } + + return current.$1.name; + }); final options = Struct.create() ..id = currentEngindId.toNativeUtf8().cast() ..datamodel = schema.toNativeUtf8().cast() ..base_path = nullptr - ..log_level = logLevel.name.toNativeUtf8().cast() - ..log_queries = logQueries + ..log_level = (logLevel ?? 'info').toNativeUtf8().cast() + ..log_queries = + this.options.logEmitter.definition.any((e) => e.$1 == LogLevel.query) ..log_callback = logNativeCallable.nativeFunction - ..env = json.encode(env ?? const {}).toNativeUtf8().cast() - ..ignore_env_var_errors = ignoreEnvVarErrors + ..env = json.encode(createQueryEngineEnvironment()).toNativeUtf8().cast() + ..ignore_env_var_errors = true ..native = native - ..datasource_overrides = json.encode(datasources).toNativeUtf8().cast(); + ..datasource_overrides = + json.encode(createOverwriteDatasources()).toNativeUtf8().cast(); final enginePtr = malloc>(); final errorPtr = malloc>(); @@ -99,8 +101,8 @@ class PrismaFlutterEngine extends Engine implements Finalizable { _evalEngineId++; malloc.free(errorPtr); - return PrismaFlutterEngine._(enginePtr.value, - schema: schema, datasources: datasources); + _qe = enginePtr.value; + return; } else if (status == Status.err) { final message = errorPtr.value.cast().toDartString(); cleanup(); @@ -124,6 +126,8 @@ class PrismaFlutterEngine extends Engine implements Finalizable { @override Future start() async { + if (_started) return; + final errorPtr = malloc>(); final status = _bindingsInstance.start(_qe, '00'.toNativeUtf8().cast(), errorPtr); @@ -139,14 +143,18 @@ class PrismaFlutterEngine extends Engine implements Finalizable { } malloc.free(errorPtr); + _started = true; } @override Future stop() async { + if (!_started) return; + final status = _bindingsInstance.stop(_qe, nullptr); if (status != Status.ok) { throw StateError('Could not stop from prisma query engine'); } + _started = false; } Future applyMigrations( @@ -231,6 +239,8 @@ class PrismaFlutterEngine extends Engine implements Finalizable { int maxWait = 2000, int timeout = 5000, TransactionIsolationLevel? isolationLevel}) async { + await start(); + final Pointer trace = headers.traceparent?.toNativeUtf8().cast() ?? nullptr; final options = json.encode({ @@ -257,6 +267,8 @@ class PrismaFlutterEngine extends Engine implements Finalizable { @override Future request(JsonQuery query, {TransactionHeaders? headers, Transaction? transaction}) async { + await start(); + final Pointer body = json.encode(query.toJson()).toNativeUtf8().cast(); final Pointer trace = @@ -293,3 +305,60 @@ class PrismaFlutterEngine extends Engine implements Finalizable { throw UnimplementedError('Prisma Flutter engine not support metrics.'); } } + +extension on LibraryEngine { + Map createQueryEngineEnvironment() { + final environment = Map.from(Prisma.environment); + + // log queries + if (options.logEmitter.definition.any((e) => e.$1 == LogLevel.query)) { + environment['LOG_QUERIES'] = 'true'; + } + + // no color + if (!Prisma.envAsBoolean('NO_COLOR') && + options.errorFormat == ErrorFormat.pretty) { + environment['CLICOLOR_FORCE'] = "1"; + } + + environment['RUST_BACKTRACE'] = Prisma.env('RUST_BACKTRACE').or(() => '1'); + environment['RUST_LOG'] = + Prisma.env('RUST_LOG').or(() => LogLevel.info.name); + + return environment; + } + + /// Creates overwrite datasources string. + Map createOverwriteDatasources() { + return datasources.map((name, datasource) { + if (options.datasourceUrl != null) { + return MapEntry(name, options.datasourceUrl!); + } + + if (options.datasources?.containsKey(name) == true) { + return MapEntry(name, options.datasources![name]!); + } + + final url = switch (datasource) { + Datasource(type: DatasourceType.url, value: final url) => url, + Datasource(type: DatasourceType.environment, value: final name) => + Prisma.env(name).or( + () => throw PrismaClientInitializationError( + errorCode: "P1013", + message: 'The environment variable "$name" does not exist', + ), + ), + }; + + return MapEntry(name, Prisma.validateDatasourceURL(url, isPorxy: false)); + }); + } +} + +extension on T? { + T or(T Function() fn) { + if (this != null) return this as T; + + return fn(); + } +} From e77d1280b7d4ce6103d938752a551e2904add0fd Mon Sep 17 00:00:00 2001 From: Seven Du Date: Fri, 7 Jun 2024 04:06:38 +0800 Subject: [PATCH 17/20] docs: upgrade guides --- packages/orm/CHANGELOG.md => CHANGELOG.md | 14 ++- docs/.vitepress/config.mts | 13 +-- .../deployment.md} | 2 +- docs/getting-started/flutter.md | 71 ++++---------- docs/getting-started/upgrade_guides.md | 97 +++++++++++++++++++ docs/index.md | 2 +- packages/orm/lib/src/base_prisma_client.dart | 3 + packages/orm_flutter/CHANGELOG.md | 26 ----- packages/orm_flutter/pubspec.yaml | 4 +- 9 files changed, 139 insertions(+), 93 deletions(-) rename packages/orm/CHANGELOG.md => CHANGELOG.md (65%) rename docs/{deployment/docker.md => getting-started/deployment.md} (99%) create mode 100644 docs/getting-started/upgrade_guides.md delete mode 100644 packages/orm_flutter/CHANGELOG.md diff --git a/packages/orm/CHANGELOG.md b/CHANGELOG.md similarity index 65% rename from packages/orm/CHANGELOG.md rename to CHANGELOG.md index 855ce36d..76255750 100644 --- a/packages/orm/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Prisma Dart v5.0.0 +# Prisma Dart v5.0.0 & Flutter integration 0.1.0 To install Prisma ORM for Dart v5.0.0 run this command: @@ -13,6 +13,15 @@ dependencies: orm: ^5.0.0 ``` +--- + +FLutter integration: + +```yaml +dependencies: + orm_flutter: ^0.1.0 +``` + ## What's Changed * refactor: throws prisma client errors @@ -20,5 +29,6 @@ dependencies: * refactor(client): add base client and client options * feat(client): add `env` support * feat(client): add logging +* refactor(Flutter integration): Refactoring the Flutter integration engine. -**Full Changelog**: https://github.com/medz/prisma-dart/compare/orm-v4.4.0...orm-v5.0.0 +**Full Changelog**: https://github.com/medz/prisma-dart/compare/orm-v4.4.0...orm-v5.0.0+orm_flutter-v0.1.0 diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 431058e0..7a7fcd35 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -6,7 +6,7 @@ export default defineConfig({ }, title: "Prisma Dart", description: - "Prisma Client Dart is an auto-generated type-safe ORM. It uses Prisma Engine as the data access layer and is as consistent as possible with the Prisma Prisma Client JS/TS APIs.", + "Prisma Client Dart is an auto-generated type-safe ORM. It uses Prisma Engine as the data access layer and is as consistent as possible with the Prisma Client JS/TS APIs.", lastUpdated: true, head: [ [ @@ -60,6 +60,8 @@ export default defineConfig({ { text: "Setup", link: "/getting-started/setup" }, { text: "Schema", link: "/getting-started/schema" }, { text: "Flutter Integration", link: "/getting-started/flutter" }, + { text: "Deployment", link: "/getting-started/deployment" }, + { text: "Upgrade Guides", link: "/getting-started/upgrade_guides" }, ], }, { @@ -104,15 +106,6 @@ export default defineConfig({ }, ], }, - { - text: "Deployment", - items: [ - { - text: "Docker", - link: "/deployment/docker", - }, - ], - }, ], }, }); diff --git a/docs/deployment/docker.md b/docs/getting-started/deployment.md similarity index 99% rename from docs/deployment/docker.md rename to docs/getting-started/deployment.md index b89602d1..5ed6a11c 100644 --- a/docs/deployment/docker.md +++ b/docs/getting-started/deployment.md @@ -1,4 +1,4 @@ -# Docker +# Deployment This page presents how to deploy Dart server application into a docker container avoiding common issues. diff --git a/docs/getting-started/flutter.md b/docs/getting-started/flutter.md index a1e16495..70d63650 100644 --- a/docs/getting-started/flutter.md +++ b/docs/getting-started/flutter.md @@ -47,24 +47,16 @@ dependencies: ## Integration -In the regular method of `orm`, we use `PrismaClient(...)` to create the client, but this does not work in Flutter, so we now use the `PrismaClient.use` method to create the client: +Set your generator engine type to `flutter` in your Prisma schema (`schema.prisma`): -```dart -import '/client.dart'; - -final prisma = PrismaClient.use((schema, datasources) { - final dbFilePath = ""; - - return engine = PrismaFlutterEngine(schema: schema, datasources: { - ...datasources, - 'db': 'file:$dbFilePath', - // ...More options - }); -}); +```prisma +generator client { + provider = "dart run orm" + output = "../lib/_generated_prisma_client" + engineType = "flutter" // [!code focus] +} ``` -> Note: In Flutter, the client will not automatically connect to the database, and you need to manually call `prisma.$connect()` to establish a connection with the database. - ## Migrations Unlike server-side, databases in Flutter are not typically handled by you in the Prisma CLI before building. @@ -106,7 +98,7 @@ flutter: ### Run migration ```dart -final engine = PrismaFlutterEngine(...); +final engine = prisma.$engine as LibraryEngine; await engine.applyMigrations( path: 'prisma/migrations/', // Your define in `flutter.assets` .igrations root dir @@ -140,31 +132,30 @@ We install the database in the `/database.sqlite` ```dart [lib/prisma.dart] import 'package:flutter/widgets.dart'; +import 'package:orm_flutter/orm_flutter.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:orm_flutter/orm_flutter.dart'; import '_generated_prisma_client/client.dart'; late final PrismaClient prisma; Future initPrismaClient() async { - WidgetsFlutterBinding.ensureInitialized(); + WidgetsFlutterBinding.ensureInitialized(); - final supportDir = await getApplicationSupportDirectory(); - final database = join(supportDir.path, 'database.sqlite'); + final supportDir = await getApplicationSupportDirectory(); + final database = join(supportDir.path, 'database.sqlite.db'); - late final PrismaFlutterEngine engine; - prisma = PrismaClient.use((schema, datasources) { - return engine = PrismaFlutterEngine(schema: schema, datasources: { - ...datasources, - 'db': 'file:$database', - }); - }); + prisma = PrismaClient(datasourceUrl: 'file:$database'); + final engine = switch (prisma.$engine) { + LibraryEngine engine => engine, + _ => null, + }; - await prisma.$connect(); - await engine.applyMigrations(path: 'prisma/migrations'); + await prisma.$connect(); + await engine?.applyMigrations(path: 'prisma/migrations'); } + ``` ```dart [lib/main.dart] @@ -179,28 +170,6 @@ Future main() async { ::: -## Platform specific engines - -The generator will generate references to all engines by default, but usually you will not use other engines. If you only want to use the Flutter integration engine, you should configure the engine type in the Prisma schema: - -::: code-group - -```prisma [schema.prisma] -generator client { - provider = "dart run orm" - engineType = "flutter" // [!code focus] -} - -datasource db { - provider = "sqlite" - url = "file:./db.sqlite" -} -``` - -::: - -When you configure `engineType` in `generator` to `flutter`, the binary engine will no longer be referenced. - ## Example App We provide you with a demo App that integrates Prisma ORM in Flutter 👉 [Flutter with ORM](https://github.com/medz/prisma-dart/tree/main/examples/flutter_with_orm) diff --git a/docs/getting-started/upgrade_guides.md b/docs/getting-started/upgrade_guides.md new file mode 100644 index 00000000..d8918824 --- /dev/null +++ b/docs/getting-started/upgrade_guides.md @@ -0,0 +1,97 @@ +# Upgrade Guides + +## Prisma Dart v4 -> v5 & Flutter integration + +Upgrading from Prisma Dart v4 to v5 is relatively simple. Although it is a major version change, there are no changes to user APIs. + +### `engineType` in Prisma schema + +Now, in the Flutter integration, you tell the generator that you want to generate a client for Flutter: + +```prisma +generator client { + provider = "dart run orm" + output = "../lib/_generated_prisma_client" + engineType = "flutter" // [!code focus] +} +``` + +### `PrismaClient` + +Major changes to Prisma Client: + +1. Changed from an isolated type to a self-dependent type with `Class PrismaClient extends BasePrismaClient`. +2. Deprecated `PrismaClient.use` method. + +Before: + +```dart +final prisma = PrismaClient.use((schema, datasoruce) { + /// Your codes, create engine and returns. +}); +``` + +Now: + +```dart +final engine = ...; // You created engine instandce. +final prisma = PrismaClient(engine: engine); +``` + +### Logging + +Previously, we relied on the `logging` package for logging. Then you needed to listen for the logs and output them yourself. + +Now, Prisma Dart implements the same logging as the Prisma TS/JS client. + +For more loging information, see 👉 [Prisma Logging](https://www.prisma.io/docs/orm/prisma-client/observability-and-logging/logging). + +#### Output to console + +Before: + +```dart +import 'package:loging/loging.dart'; + +Logger.root.onRecore.listen((log) { + print(log.message); +}); +``` + +Now: + +```dart +final prisma = PrismaClient( + log: { + (LogLevel.query, LogEmit.stdout), + // ... More level configs + }, +); +``` + +#### Log event + +Before: + +```dart +import 'package:loging/loging.dart'; + +Logger.root.onRecore.listen((log) { + //... You custon. +}); +``` + +Now: + +```dart +final prisma = PrismaClient( + log: { + (LogLevel.query, LogEmit.event), + // ... More level configs + }, +); + +prisma.$on(LogLevel.query, (event) { + // ... +}); +``` diff --git a/docs/index.md b/docs/index.md index c22a144b..c439d8cd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,7 +5,7 @@ layout: home hero: name: Prisma Client Dart text: Auto-generated type-safe ORM - tagline: It uses Prisma Engine as the data access layer and is as consistent as possible with the Prisma Prisma Client JS/TS APIs + tagline: It uses Prisma Engine as the data access layer and is as consistent as possible with the Prisma Client JS/TS APIs image: src: /prisma-dart.logo.svg alt: Prisma Dart client logo diff --git a/packages/orm/lib/src/base_prisma_client.dart b/packages/orm/lib/src/base_prisma_client.dart index 4a4d0fd5..a526e297 100644 --- a/packages/orm/lib/src/base_prisma_client.dart +++ b/packages/orm/lib/src/base_prisma_client.dart @@ -34,4 +34,7 @@ abstract class BasePrismaClient> { Future $connect() => $engine.start(); Future $disconnect() => $engine.stop(); + + void $on(LogLevel level, void Function(EngineEvent event) listener) => + $options.logEmitter.on(level, listener); } diff --git a/packages/orm_flutter/CHANGELOG.md b/packages/orm_flutter/CHANGELOG.md deleted file mode 100644 index 34bcab10..00000000 --- a/packages/orm_flutter/CHANGELOG.md +++ /dev/null @@ -1,26 +0,0 @@ -# 0.0.6 - -* Fix downloading query engine before building - -# 0.0.5 - -## What's Changed - -* Download static libraries during construction - -# 0.0.4 - -## What's Changed - -* Support Android - -# 0.0.3 - -* fixed package name. - -# 0.0.2 - -## What's Changed - -* fixed `orm_flutter` reanme. -* reanem `prisma_flutter` to `engine_bridge` diff --git a/packages/orm_flutter/pubspec.yaml b/packages/orm_flutter/pubspec.yaml index 02b910b6..c7267a8f 100644 --- a/packages/orm_flutter/pubspec.yaml +++ b/packages/orm_flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: orm_flutter description: "The engine of Prisma ORM for Flutter, Library for binding Prisma's C-Abi engine with Flutter." -version: 0.0.6 +version: 0.1.0 homepage: https://prisma.pub/getting-started/flutter.html repository: https://github.com/medz/prisma-dart funding: @@ -15,7 +15,7 @@ dependencies: ffi: ^2.1.2 flutter: sdk: flutter - orm: ^4.1.0 + orm: ^5.0.0 package_config: ^2.1.0 path: ^1.9.0 path_provider: ^2.1.3 From 2d2ff8417695861380957ffc7aa412171b60f33c Mon Sep 17 00:00:00 2001 From: Seven Du Date: Fri, 7 Jun 2024 04:08:39 +0800 Subject: [PATCH 18/20] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76255750..a96ea709 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ dependencies: orm_flutter: ^0.1.0 ``` +[**"Prisma Dart v4 -> v5 & Flutter integration" Upgrade Guides**](https://prisma.pub/getting-started/upgrade_guides.html) + ## What's Changed * refactor: throws prisma client errors From 0ffdfb5e588e950bcced7e605909f3c18d062f26 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Fri, 7 Jun 2024 04:10:55 +0800 Subject: [PATCH 19/20] chore: link changelog --- packages/orm/CHANGELOG.md | 1 + packages/orm_flutter/CHANGELOG.md | 1 + 2 files changed, 2 insertions(+) create mode 120000 packages/orm/CHANGELOG.md create mode 120000 packages/orm_flutter/CHANGELOG.md diff --git a/packages/orm/CHANGELOG.md b/packages/orm/CHANGELOG.md new file mode 120000 index 00000000..699cc9e7 --- /dev/null +++ b/packages/orm/CHANGELOG.md @@ -0,0 +1 @@ +../../CHANGELOG.md \ No newline at end of file diff --git a/packages/orm_flutter/CHANGELOG.md b/packages/orm_flutter/CHANGELOG.md new file mode 120000 index 00000000..699cc9e7 --- /dev/null +++ b/packages/orm_flutter/CHANGELOG.md @@ -0,0 +1 @@ +../../CHANGELOG.md \ No newline at end of file From 252d0a5cf77323b2629d0346c7fc29f9d068109f Mon Sep 17 00:00:00 2001 From: Seven Du Date: Fri, 7 Jun 2024 04:12:49 +0800 Subject: [PATCH 20/20] chore: cleanup --- docs/getting-started/upgrade_guides.md | 2 +- packages/orm/pubspec.yaml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/getting-started/upgrade_guides.md b/docs/getting-started/upgrade_guides.md index d8918824..44809751 100644 --- a/docs/getting-started/upgrade_guides.md +++ b/docs/getting-started/upgrade_guides.md @@ -44,7 +44,7 @@ Previously, we relied on the `logging` package for logging. Then you needed to l Now, Prisma Dart implements the same logging as the Prisma TS/JS client. -For more loging information, see 👉 [Prisma Logging](https://www.prisma.io/docs/orm/prisma-client/observability-and-logging/logging). +For more logging information, see 👉 [Prisma Logging](https://www.prisma.io/docs/orm/prisma-client/observability-and-logging/logging). #### Output to console diff --git a/packages/orm/pubspec.yaml b/packages/orm/pubspec.yaml index ae5246e0..14d1bb85 100644 --- a/packages/orm/pubspec.yaml +++ b/packages/orm/pubspec.yaml @@ -18,7 +18,6 @@ dependencies: dart_style: ^2.3.4 decimal: ^3.0.0 json_rpc_2: ^3.0.2 - logging: ^1.2.0 path: ^1.8.3 rc: ^0.3.3 recase: ^4.1.0