diff --git a/angel_orm/CHANGELOG.md b/angel_orm/CHANGELOG.md index f6f92795..5b6624c2 100644 --- a/angel_orm/CHANGELOG.md +++ b/angel_orm/CHANGELOG.md @@ -1,3 +1,9 @@ +# 1.0.0-alpha+11 +* Removed PostgreSQL-specific functionality, so that the ORM can ultimately +target all services. +* Created a better `Join` model. +* Created a far better `Query` model. + # 1.0.0-alpha+10 * Split into `angel_orm.dart` and `server.dart`. Prevents DDC failures. diff --git a/angel_orm/README.md b/angel_orm/README.md index 5546e559..5a15df43 100644 --- a/angel_orm/README.md +++ b/angel_orm/README.md @@ -1,7 +1,6 @@ # angel_orm -Runtime support for Angel's ORM. Includes SQL expression generators, as well -as a friendly `PostgreSQLConnectionPool` class that you can use to pool connections -to a PostgreSQL database. +Runtime support for Angel's ORM. Includes a clean, database-agnostic +query builder and relationship/join support. For documentation about the ORM, head to the main project repo: https://github.com/angel-dart/orm \ No newline at end of file diff --git a/angel_orm/example/main.dart b/angel_orm/example/main.dart new file mode 100644 index 00000000..40b6ae01 --- /dev/null +++ b/angel_orm/example/main.dart @@ -0,0 +1,27 @@ +import 'package:angel_model/angel_model.dart'; +import 'package:angel_orm/angel_orm.dart'; + +Query findEmployees(Company company) { + return new Query() + ..['company_id'] = equals(company.id) + ..['first_name'] = notNull() & (equals('John')) + ..['salary'] = greaterThanOrEqual(100000.0); +} + +@ORM('api/companies') +class Company extends Model { + String name; + bool isFortune500; +} + +@orm +class Employee extends Model { + @belongsTo + Company company; + + String firstName, lastName; + + double salary; + + bool get isFortune500Employee => company.isFortune500; +} diff --git a/angel_orm/lib/angel_orm.dart b/angel_orm/lib/angel_orm.dart index 1af57ece..9243760e 100644 --- a/angel_orm/lib/angel_orm.dart +++ b/angel_orm/lib/angel_orm.dart @@ -1,4 +1,3 @@ export 'src/annotations.dart'; -export 'src/migration.dart'; -export 'src/relations.dart'; -export 'src/query.dart'; \ No newline at end of file +export 'src/query.dart'; +export 'src/relations.dart'; \ No newline at end of file diff --git a/angel_orm/lib/src/annotations.dart b/angel_orm/lib/src/annotations.dart index dbe16ad3..677ce0a1 100644 --- a/angel_orm/lib/src/annotations.dart +++ b/angel_orm/lib/src/annotations.dart @@ -1,12 +1,31 @@ const ORM orm = const ORM(); class ORM { - final String tableName; - const ORM([this.tableName]); + /// The path to an Angel service that queries objects of the + /// annotated type at runtime. + /// + /// Ex. `api/users`, etc. + final String servicePath; + const ORM([this.servicePath]); } -class CanJoin { +/// Specifies that the ORM should build a join builder +/// that combines the results of queries on two services. +class Join { + /// The [Model] type to join against. final Type type; - final String foreignKey; - const CanJoin(this.type, this.foreignKey); -} \ No newline at end of file + + /// The path to an Angel service that queries objects of the + /// [type] being joined against, at runtime. + /// + /// Ex. `api/users`, etc. + final String servicePath; + + /// The type of join this is. + final JoinType joinType; + + const Join(this.type, this.servicePath, [this.joinType = JoinType.join]); +} + +/// The various types of [Join]. +enum JoinType { join, left, right, full, self } diff --git a/angel_orm/lib/src/migration.dart b/angel_orm/lib/src/migration.dart deleted file mode 100644 index 80620fdb..00000000 --- a/angel_orm/lib/src/migration.dart +++ /dev/null @@ -1,113 +0,0 @@ -const List SQL_RESERVED_WORDS = const [ - 'SELECT', 'UPDATE', 'INSERT', 'DELETE', 'FROM', 'ASC', 'DESC', 'VALUES', 'RETURNING', 'ORDER', 'BY', -]; - -/// Applies additional attributes to a database column. -class Column { - /// If `true`, a SQL field will be nullable. - final bool nullable; - - /// Specifies the length of a `VARCHAR`. - final int length; - - /// Explicitly defines a SQL type for this column. - final ColumnType type; - - /// Specifies what kind of index this column is, if any. - final IndexType index; - - /// The default value of this field. - final defaultValue; - - const Column( - {this.nullable: true, - this.length, - this.type, - this.index: IndexType.NONE, - this.defaultValue}); -} - -class PrimaryKey extends Column { - const PrimaryKey({ColumnType columnType}) - : super( - type: columnType ?? ColumnType.SERIAL, - index: IndexType.PRIMARY_KEY); -} - -const Column primaryKey = const PrimaryKey(); - -/// Maps to SQL index types. -enum IndexType { - NONE, - - /// Standard index. - INDEX, - - /// A primary key. - PRIMARY_KEY, - - /// A *unique* index. - UNIQUE -} - -/// Maps to SQL data types. -/// -/// Features all types from this list: http://www.tutorialspoint.com/sql/sql-data-types.htm -class ColumnType { - /// The name of this data type. - final String name; - const ColumnType(this.name); - - static const ColumnType BOOLEAN = const ColumnType('boolean'); - - static const ColumnType SMALL_SERIAL = const ColumnType('smallserial'); - static const ColumnType SERIAL = const ColumnType('serial'); - static const ColumnType BIG_SERIAL = const ColumnType('bigserial'); - - // Numbers - static const ColumnType BIG_INT = const ColumnType('bigint'); - static const ColumnType INT = const ColumnType('int'); - static const ColumnType SMALL_INT = const ColumnType('smallint'); - static const ColumnType TINY_INT = const ColumnType('tinyint'); - static const ColumnType BIT = const ColumnType('bit'); - static const ColumnType DECIMAL = const ColumnType('decimal'); - static const ColumnType NUMERIC = const ColumnType('numeric'); - static const ColumnType MONEY = const ColumnType('money'); - static const ColumnType SMALL_MONEY = const ColumnType('smallmoney'); - static const ColumnType FLOAT = const ColumnType('float'); - static const ColumnType REAL = const ColumnType('real'); - - // Dates and times - static const ColumnType DATE_TIME = const ColumnType('datetime'); - static const ColumnType SMALL_DATE_TIME = const ColumnType('smalldatetime'); - static const ColumnType DATE = const ColumnType('date'); - static const ColumnType TIME = const ColumnType('time'); - static const ColumnType TIME_STAMP = const ColumnType('timestamp'); - static const ColumnType TIME_STAMP_WITH_TIME_ZONE = const ColumnType('timestamp with time zone'); - - // Strings - static const ColumnType CHAR = const ColumnType('char'); - static const ColumnType VAR_CHAR = const ColumnType('varchar'); - static const ColumnType VAR_CHAR_MAX = const ColumnType('varchar(max)'); - static const ColumnType TEXT = const ColumnType('text'); - - // Unicode strings - static const ColumnType NCHAR = const ColumnType('nchar'); - static const ColumnType NVAR_CHAR = const ColumnType('nvarchar'); - static const ColumnType NVAR_CHAR_MAX = const ColumnType('nvarchar(max)'); - static const ColumnType NTEXT = const ColumnType('ntext'); - - // Binary - static const ColumnType BINARY = const ColumnType('binary'); - static const ColumnType VAR_BINARY = const ColumnType('varbinary'); - static const ColumnType VAR_BINARY_MAX = const ColumnType('varbinary(max)'); - static const ColumnType IMAGE = const ColumnType('image'); - - // Misc. - static const ColumnType SQL_VARIANT = const ColumnType('sql_variant'); - static const ColumnType UNIQUE_IDENTIFIER = - const ColumnType('uniqueidentifier'); - static const ColumnType XML = const ColumnType('xml'); - static const ColumnType CURSOR = const ColumnType('cursor'); - static const ColumnType TABLE = const ColumnType('table'); -} diff --git a/angel_orm/lib/src/pool.dart b/angel_orm/lib/src/pool.dart deleted file mode 100644 index da77b854..00000000 --- a/angel_orm/lib/src/pool.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'dart:async'; -import 'package:pool/pool.dart'; -import 'package:postgres/postgres.dart'; - -/// Connects to a PostgreSQL database, whether synchronously or asynchronously. -typedef FutureOr PostgreSQLConnector(); - -/// Pools connections to a PostgreSQL database. -class PostgreSQLConnectionPool { - final List _connections = []; - final List _opened = []; - int _index = 0; - Pool _pool; - - /// The maximum number of concurrent connections to the database. - /// - /// Default: `5` - final int concurrency; - - /// An optional timeout for pooled connections to execute. - final Duration timeout; - - /// A function that connects this pool to the database, on-demand. - final PostgreSQLConnector connector; - - PostgreSQLConnectionPool(this.connector, - {this.concurrency: 5, this.timeout}) { - _pool = new Pool(concurrency, timeout: timeout); - } - - Future _connect() async { - if (_connections.isEmpty) { - for (int i = 0; i < concurrency; i++) { - _connections.add(await connector()); - } - } - - var connection = _connections[_index++]; - if (_index >= _connections.length) _index = 0; - - if (!_opened.contains(connection.hashCode)) { - await connection.open(); - _opened.add(connection.hashCode); - } - - return connection; - } - - Future close() => Future.wait(_connections.map((c) => c.close())); - - /// Connects to the database, and then executes the [callback]. - /// - /// Returns the result of [callback]. - Future run(FutureOr callback(PostgreSQLConnection connection)) { - return _pool.request().then((resx) { - return _connect().then((connection) { - return new Future.sync(() => callback(connection)) - .whenComplete(() async { - if (connection.isClosed) { - _connections - ..remove(connection) - ..add(await connector()); - } - resx.release(); - }); - }); - }); - } -} diff --git a/angel_orm/lib/src/query.dart b/angel_orm/lib/src/query.dart index 0060f40e..cefb89fa 100644 --- a/angel_orm/lib/src/query.dart +++ b/angel_orm/lib/src/query.dart @@ -1,332 +1,114 @@ -import 'package:meta/meta.dart'; -import 'package:intl/intl.dart'; -import 'package:string_scanner/string_scanner.dart'; +/// Expects a field to be equal to a given [value]. +Predicate equals(T value) => + new Predicate._(PredicateType.equals, value); -final DateFormat DATE_YMD = new DateFormat('yyyy-MM-dd'); -final DateFormat DATE_YMD_HMS = new DateFormat('yyyy-MM-dd HH:mm:ss'); +/// Expects at least one of the given [predicates] to be true. +Predicate anyOf(Iterable> predicates) => + new MultiPredicate._(PredicateType.any, predicates); -/// Cleans an input SQL expression of common SQL injection points. -String sanitizeExpression(String unsafe) { - var buf = new StringBuffer(); - var scanner = new StringScanner(unsafe); - int ch; +/// Expects a field to be contained within a set of [values]. +Predicate isIn(Iterable values) => new Predicate._(PredicateType.isIn, null, values); - while (!scanner.isDone) { - // Ignore comment starts - if (scanner.scan('--') || scanner.scan('/*')) - continue; +/// Expects a field to be `null`. +Predicate isNull() => equals(null); - // Ignore all single quotes and attempted escape sequences - else if (scanner.scan("'") || scanner.scan('\\')) - continue; +/// Expects a given [predicate] to not be true. +Predicate not(Predicate predicate) => + new MultiPredicate._(PredicateType.negate, [predicate]); - // Otherwise, add the next char, unless it's a null byte. - else if ((ch = scanner.readChar()) != 0 && ch != null) - buf.writeCharCode(ch); - } +/// Expects a field to be not be `null`. +Predicate notNull() => not(isNull()); - return buf.toString(); +/// Expects a field to be less than a given [value]. +Predicate lessThan(T value) => + new Predicate._(PredicateType.less, value); + +/// Expects a field to be less than or equal to a given [value]. +Predicate lessThanOrEqual(T value) => lessThan(value) | equals(value); + +/// Expects a field to be greater than a given [value]. +Predicate greaterThan(T value) => + new Predicate._(PredicateType.greater, value); + +/// Expects a field to be greater than or equal to a given [value]. +Predicate greaterThanOrEqual(T value) => + greaterThan(value) | equals(value); + +/// A generic query class. +/// +/// Angel services can translate these into driver-specific queries. +/// This allows the Angel ORM to be flexible and support multiple platforms. +class Query { + final Map _fields = {}; + final Map _sort = {}; + + /// Each field in a query is actually a [Predicate], and therefore acts as a contract + /// with the underlying service. + Map get fields => + new Map.unmodifiable(_fields); + + /// The sorting order applied to this query. + Map get sorting => + new Map.unmodifiable(_sort); + + /// Sets the [Predicate] assigned to the given [key]. + void operator []=(String key, Predicate value) => _fields[key] = value; + + /// Gets the [Predicate] assigned to the given [key]. + Predicate operator [](String key) => _fields[key]; + + /// Sort output by the given [key]. + void sortBy(String key, [SortType type = SortType.descending]) => + _sort[key] = type; } -abstract class SqlExpressionBuilder { - bool get hasValue; - String compile(); - void isBetween(lower, upper); - void isNotBetween(lower, upper); - void isIn(Iterable values); - void isNotIn(Iterable values); -} +/// A mechanism used to express an expectation about some object ([target]). +class Predicate { + /// The type of expectation we are declaring. + final PredicateType type; -class NumericSqlExpressionBuilder - implements SqlExpressionBuilder { - bool _hasValue = false; - String _op = '='; - String _raw; - T _value; + /// The single argument of this target. + final T target; + final Iterable args; - @override - bool get hasValue => _hasValue; + Predicate._(this.type, this.target, [this.args]); - bool _change(String op, T value) { - _raw = null; - _op = op; - _value = value; - return _hasValue = true; + Predicate operator &(Predicate other) => and(other); + + Predicate operator |(Predicate other) => or(other); + + Predicate and(Predicate other) { + return new MultiPredicate._(PredicateType.and, [this, other]); } - @override - String compile() { - if (_raw != null) return _raw; - if (_value == null) return null; - return '$_op $_value'; - } - - operator <(T value) => _change('<', value); - operator >(T value) => _change('>', value); - operator <=(T value) => _change('<=', value); - operator >=(T value) => _change('>=', value); - - void lessThan(T value) { - _change('<', value); - } - - void lessThanOrEqualTo(T value) { - _change('<=', value); - } - - void greaterThan(T value) { - _change('>', value); - } - - void greaterThanOrEqualTo(T value) { - _change('>=', value); - } - - void equals(T value) { - _change('=', value); - } - - void notEquals(T value) { - _change('!=', value); - } - - @override - void isBetween(@checked T lower, @checked T upper) { - _raw = 'BETWEEN $lower AND $upper'; - _hasValue = true; - } - - @override - void isNotBetween(@checked T lower, @checked T upper) { - _raw = 'NOT BETWEEN $lower AND $upper'; - _hasValue = true; - } - - @override - void isIn(@checked Iterable values) { - _raw = 'IN (' + values.join(', ') + ')'; - _hasValue = true; - } - - @override - void isNotIn(@checked Iterable values) { - _raw = 'NOT IN (' + values.join(', ') + ')'; - _hasValue = true; + Predicate or(Predicate other) { + return new MultiPredicate._(PredicateType.or, [this, other]); } } -class StringSqlExpressionBuilder implements SqlExpressionBuilder { - bool _hasValue = false; - String _op = '=', _raw, _value; +/// An advanced [Predicate] that performs an operation of multiple other predicates. +class MultiPredicate extends Predicate { + final Iterable> targets; - @override - bool get hasValue => _hasValue; + MultiPredicate._(PredicateType type, this.targets) : super._(type, null); - bool _change(String op, String value) { - _raw = null; - _op = op; - _value = value; - return _hasValue = true; - } - - @override - String compile() { - if (_raw != null) return _raw; - if (_value == null) return null; - var v = sanitizeExpression(_value); - return "$_op '$v'"; - } - - void isEmpty() => equals(''); - - void equals(String value) { - _change('=', value); - } - - void notEquals(String value) { - _change('!=', value); - } - - void like(String value) { - _change('LIKE', value); - } - - @override - void isBetween(@checked String lower, @checked String upper) { - var l = sanitizeExpression(lower), u = sanitizeExpression(upper); - _raw = "BETWEEN '$l' AND '$u'"; - _hasValue = true; - } - - @override - void isNotBetween(@checked String lower, @checked String upper) { - var l = sanitizeExpression(lower), u = sanitizeExpression(upper); - _raw = "NOT BETWEEN '$l' AND '$u'"; - _hasValue = true; - } - - @override - void isIn(@checked Iterable values) { - _raw = 'IN (' + - values.map(sanitizeExpression).map((s) => "'$s'").join(', ') + - ')'; - _hasValue = true; - } - - @override - void isNotIn(@checked Iterable values) { - _raw = 'NOT IN (' + - values.map(sanitizeExpression).map((s) => "'$s'").join(', ') + - ')'; - _hasValue = true; - } + /// Use [targets] instead. + @deprecated + T get target => throw new UnsupportedError( + 'IterablePredicate has no `target`. Use `targets` instead.'); } -class BooleanSqlExpressionBuilder implements SqlExpressionBuilder { - bool _hasValue = false; - String _op = '=', _raw; - bool _value; - - @override - bool get hasValue => _hasValue; - - bool _change(String op, bool value) { - _raw = null; - _op = op; - _value = value; - return _hasValue = true; - } - - @override - String compile() { - if (_raw != null) return _raw; - if (_value == null) return null; - var v = _value ? 'TRUE' : 'FALSE'; - return '$_op $v'; - } - - void equals(bool value) { - _change('=', value); - } - - void notEquals(bool value) { - _change('!=', value); - } - - @override - void isBetween(@checked bool lower, @checked bool upper) => - throw new UnsupportedError( - 'Booleans do not support BETWEEN expressions.'); - - @override - void isNotBetween(@checked bool lower, @checked bool upper) => - isBetween(lower, upper); - - @override - void isIn(@checked Iterable values) { - _raw = 'IN (' + values.map((b) => b ? 'TRUE' : 'FALSE').join(', ') + ')'; - _hasValue = true; - } - - @override - void isNotIn(@checked Iterable values) { - _raw = - 'NOT IN (' + values.map((b) => b ? 'TRUE' : 'FALSE').join(', ') + ')'; - _hasValue = true; - } +/// The various types of predicate. +enum PredicateType { + equals, + any, + isIn, + negate, + and, + or, + less, + greater, } -class DateTimeSqlExpressionBuilder implements SqlExpressionBuilder { - final NumericSqlExpressionBuilder year = - new NumericSqlExpressionBuilder(), - month = new NumericSqlExpressionBuilder(), - day = new NumericSqlExpressionBuilder(), - hour = new NumericSqlExpressionBuilder(), - minute = new NumericSqlExpressionBuilder(), - second = new NumericSqlExpressionBuilder(); - final String columnName; - String _raw; - - DateTimeSqlExpressionBuilder(this.columnName); - - @override - bool get hasValue => - _raw?.isNotEmpty == true || - year.hasValue || - month.hasValue || - day.hasValue || - hour.hasValue || - minute.hasValue || - second.hasValue; - - bool _change(String _op, DateTime dt, bool time) { - var dateString = time ? DATE_YMD_HMS.format(dt) : DATE_YMD.format(dt); - _raw = '$columnName $_op \'$dateString\''; - return true; - } - - operator <(DateTime value) => _change('<', value, true); - operator <=(DateTime value) => _change('<=', value, true); - operator >(DateTime value) => _change('>', value, true); - operator >=(DateTime value) => _change('>=', value, true); - - void equals(DateTime value, {bool includeTime: true}) { - _change('=', value, includeTime != false); - } - - void lessThan(DateTime value, {bool includeTime: true}) { - _change('<', value, includeTime != false); - } - - void lessThanOrEqualTo(DateTime value, {bool includeTime: true}) { - _change('<=', value, includeTime != false); - } - - void greaterThan(DateTime value, {bool includeTime: true}) { - _change('>', value, includeTime != false); - } - - void greaterThanOrEqualTo(DateTime value, {bool includeTime: true}) { - _change('>=', value, includeTime != false); - } - - @override - void isIn(@checked Iterable values) { - _raw = '$columnName IN (' + - values.map(DATE_YMD_HMS.format).map((s) => '$s').join(', ') + - ')'; - } - - @override - void isNotIn(@checked Iterable values) { - _raw = '$columnName NOT IN (' + - values.map(DATE_YMD_HMS.format).map((s) => '$s').join(', ') + - ')'; - } - - @override - void isBetween(@checked DateTime lower, @checked DateTime upper) { - var l = DATE_YMD_HMS.format(lower), u = DATE_YMD_HMS.format(upper); - _raw = "$columnName BETWEEN '$l' and '$u'"; - } - - @override - void isNotBetween(@checked DateTime lower, @checked DateTime upper) { - var l = DATE_YMD_HMS.format(lower), u = DATE_YMD_HMS.format(upper); - _raw = "$columnName NOT BETWEEN '$l' and '$u'"; - } - - @override - String compile() { - if (_raw?.isNotEmpty == true) return _raw; - List parts = []; - if (year.hasValue) parts.add('YEAR($columnName) ${year.compile()}'); - if (month.hasValue) parts.add('MONTH($columnName) ${month.compile()}'); - if (day.hasValue) parts.add('DAY($columnName) ${day.compile()}'); - if (hour.hasValue) parts.add('HOUR($columnName) ${hour.compile()}'); - if (minute.hasValue) parts.add('MINUTE($columnName) ${minute.compile()}'); - if (second.hasValue) parts.add('SECOND($columnName) ${second.compile()}'); - - return parts.isEmpty ? null : parts.join(' AND '); - } -} +/// The various modes of sorting. +enum SortType { ascending, descending } diff --git a/angel_orm/pubspec.yaml b/angel_orm/pubspec.yaml index aa5cadfd..3fd8d3ec 100644 --- a/angel_orm/pubspec.yaml +++ b/angel_orm/pubspec.yaml @@ -1,13 +1,10 @@ name: angel_orm -version: 1.0.0-alpha+12 +version: 1.0.0-alpha+11 description: Runtime support for Angel's ORM. author: Tobe O homepage: https://github.com/angel-dart/orm environment: - sdk: ">=1.19.0" + sdk: '>=2.0.0-dev.1.2 <2.0.0' dependencies: - intl: ">=0.0.0 <1.0.0" - meta: ^1.0.0 - pool: ^1.0.0 - postgres: ^0.9.5 - string_scanner: ^1.0.0 \ No newline at end of file + angel_model: ^1.0.0 + meta: ^1.0.0 \ No newline at end of file diff --git a/angel_orm_generator/test/models/order.dart b/angel_orm_generator/test/models/order.dart index 1125dd9f..7f87f134 100644 --- a/angel_orm_generator/test/models/order.dart +++ b/angel_orm_generator/test/models/order.dart @@ -9,7 +9,7 @@ part 'order.g.dart'; @orm @serializable class _Order extends Model { - @CanJoin(Customer, 'id') + @Join(Customer, 'id') int customerId; int employeeId; DateTime orderDate;