Restore old annotations
This commit is contained in:
parent
5dca761978
commit
1e234ea177
20 changed files with 506 additions and 1289 deletions
|
@ -2,10 +2,12 @@
|
|||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$/angel_orm">
|
||||
<excludeFolder url="file://$MODULE_DIR$/angel_orm/.dart_tool" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/angel_orm/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/angel_orm/build" />
|
||||
</content>
|
||||
<content url="file://$MODULE_DIR$/angel_orm_generator">
|
||||
<excludeFolder url="file://$MODULE_DIR$/angel_orm_generator/.dart_tool" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/angel_orm_generator/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/angel_orm_generator/build" />
|
||||
</content>
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
analyzer:
|
||||
strong-mode: true
|
2
angel_orm/.gitignore
vendored
2
angel_orm/.gitignore
vendored
|
@ -54,3 +54,5 @@ com_crashlytics_export_strings.xml
|
|||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
.dart_tool
|
|
@ -1,3 +1,8 @@
|
|||
# 2.0.0-dev
|
||||
* Restored all old PostgreSQL-specific annotations. Rather than a smart runtime,
|
||||
having a codegen capable of building ORM's for multiple databases can potentially
|
||||
provide a very fast ORM for everyone.
|
||||
|
||||
# 1.0.0-alpha+11
|
||||
* Removed PostgreSQL-specific functionality, so that the ORM can ultimately
|
||||
target all services.
|
||||
|
|
3
angel_orm/analysis_options.yaml
Normal file
3
angel_orm/analysis_options.yaml
Normal file
|
@ -0,0 +1,3 @@
|
|||
analyzer:
|
||||
strong-mode:
|
||||
implicit-casts: false
|
|
@ -1,27 +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);
|
||||
}
|
||||
main() {
|
||||
|
||||
@ORM('api/companies')
|
||||
class Company extends Model {
|
||||
String name;
|
||||
bool isFortune500;
|
||||
}
|
||||
|
||||
@orm
|
||||
class Employee extends Model {
|
||||
abstract class Company extends Model {
|
||||
String get name;
|
||||
|
||||
bool get isFortune500;
|
||||
}
|
||||
|
||||
@orm
|
||||
abstract class _Employee extends Model {
|
||||
@belongsTo
|
||||
Company company;
|
||||
Company get company;
|
||||
|
||||
String firstName, lastName;
|
||||
String get firstName;
|
||||
|
||||
double salary;
|
||||
String get lastName;
|
||||
|
||||
double get salary;
|
||||
|
||||
bool get isFortune500Employee => company.isFortune500;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export 'src/annotations.dart';
|
||||
export 'src/query.dart';
|
||||
export 'src/migration.dart';
|
||||
export 'src/relations.dart';
|
||||
export 'src/query.dart';
|
|
@ -1,30 +1,17 @@
|
|||
const ORM orm = const ORM();
|
||||
|
||||
class ORM {
|
||||
/// 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]);
|
||||
final String tableName;
|
||||
|
||||
const ORM([this.tableName]);
|
||||
}
|
||||
|
||||
/// 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.
|
||||
class CanJoin {
|
||||
final Type type;
|
||||
|
||||
/// 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 String foreignKey;
|
||||
final JoinType joinType;
|
||||
|
||||
const Join(this.type, this.servicePath, [this.joinType = JoinType.join]);
|
||||
const CanJoin(this.type, this.foreignKey, {this.joinType: JoinType.full});
|
||||
}
|
||||
|
||||
/// The various types of [Join].
|
||||
|
|
124
angel_orm/lib/src/migration.dart
Normal file
124
angel_orm/lib/src/migration.dart
Normal file
|
@ -0,0 +1,124 @@
|
|||
const List<String> 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 isNullable;
|
||||
|
||||
/// 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 indexType;
|
||||
|
||||
/// The default value of this field.
|
||||
final defaultValue;
|
||||
|
||||
const Column(
|
||||
{this.isNullable: true,
|
||||
this.length,
|
||||
this.type,
|
||||
this.indexType: IndexType.none,
|
||||
this.defaultValue});
|
||||
}
|
||||
|
||||
class PrimaryKey extends Column {
|
||||
const PrimaryKey({ColumnType columnType})
|
||||
: super(
|
||||
type: columnType ?? ColumnType.serial, indexType: IndexType.primaryKey);
|
||||
}
|
||||
|
||||
const Column primaryKey = const PrimaryKey();
|
||||
|
||||
/// Maps to SQL index types.
|
||||
enum IndexType {
|
||||
none,
|
||||
|
||||
/// Standard index.
|
||||
standardIndex,
|
||||
|
||||
/// A primary key.
|
||||
primaryKey,
|
||||
|
||||
/// 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 smallSerial = const ColumnType('smallserial');
|
||||
static const ColumnType serial = const ColumnType('serial');
|
||||
static const ColumnType bigSerial = const ColumnType('bigserial');
|
||||
|
||||
// Numbers
|
||||
static const ColumnType bigInt = const ColumnType('bigint');
|
||||
static const ColumnType int = const ColumnType('int');
|
||||
static const ColumnType smallInt = const ColumnType('smallint');
|
||||
static const ColumnType tinyInt = 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 smallMoney = const ColumnType('smallmoney');
|
||||
static const ColumnType float = const ColumnType('float');
|
||||
static const ColumnType real = const ColumnType('real');
|
||||
|
||||
// Dates and times
|
||||
static const ColumnType dateTime = const ColumnType('datetime');
|
||||
static const ColumnType smallDateTime = const ColumnType('smalldatetime');
|
||||
static const ColumnType date = const ColumnType('date');
|
||||
static const ColumnType time = const ColumnType('time');
|
||||
static const ColumnType timeStamp = const ColumnType('timestamp');
|
||||
static const ColumnType timeStampWithTimeZone =
|
||||
const ColumnType('timestamp with time zone');
|
||||
|
||||
// Strings
|
||||
static const ColumnType char = const ColumnType('char');
|
||||
static const ColumnType varChar = const ColumnType('varchar');
|
||||
static const ColumnType varCharMax = const ColumnType('varchar(max)');
|
||||
static const ColumnType text = const ColumnType('text');
|
||||
|
||||
// Unicode strings
|
||||
static const ColumnType nChar = const ColumnType('nchar');
|
||||
static const ColumnType nVarChar = const ColumnType('nvarchar');
|
||||
static const ColumnType nVarCharMax = const ColumnType('nvarchar(max)');
|
||||
static const ColumnType nText = const ColumnType('ntext');
|
||||
|
||||
// Binary
|
||||
static const ColumnType binary = const ColumnType('binary');
|
||||
static const ColumnType varBinary = const ColumnType('varbinary');
|
||||
static const ColumnType varBinaryMax = const ColumnType('varbinary(max)');
|
||||
static const ColumnType image = const ColumnType('image');
|
||||
|
||||
// Misc.
|
||||
static const ColumnType sqlVariant = const ColumnType('sql_variant');
|
||||
static const ColumnType uniqueIdentifier =
|
||||
const ColumnType('uniqueidentifier');
|
||||
static const ColumnType xml = const ColumnType('xml');
|
||||
static const ColumnType cursor = const ColumnType('cursor');
|
||||
static const ColumnType table = const ColumnType('table');
|
||||
}
|
|
@ -1,114 +1,340 @@
|
|||
/// Expects a field to be equal to a given [value].
|
||||
Predicate<T> equals<T>(T value) =>
|
||||
new Predicate<T>._(PredicateType.equals, value);
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:string_scanner/string_scanner.dart';
|
||||
|
||||
/// Expects at least one of the given [predicates] to be true.
|
||||
Predicate<T> anyOf<T>(Iterable<Predicate<T>> predicates) =>
|
||||
new MultiPredicate<T>._(PredicateType.any, predicates);
|
||||
final DateFormat dateYmd = new DateFormat('yyyy-MM-dd');
|
||||
final DateFormat dateYmdHms = new DateFormat('yyyy-MM-dd HH:mm:ss');
|
||||
|
||||
/// Expects a field to be contained within a set of [values].
|
||||
Predicate<T> isIn<T>(Iterable<T> values) => new Predicate<T>._(PredicateType.isIn, null, values);
|
||||
/// 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 `null`.
|
||||
Predicate<T> isNull<T>() => equals(null);
|
||||
while (!scanner.isDone) {
|
||||
// Ignore comment starts
|
||||
if (scanner.scan('--') || scanner.scan('/*'))
|
||||
continue;
|
||||
|
||||
/// Expects a given [predicate] to not be true.
|
||||
Predicate<T> not<T>(Predicate<T> predicate) =>
|
||||
new MultiPredicate<T>._(PredicateType.negate, [predicate]);
|
||||
// Ignore all single quotes and attempted escape sequences
|
||||
else if (scanner.scan("'") || scanner.scan('\\'))
|
||||
continue;
|
||||
|
||||
/// Expects a field to be not be `null`.
|
||||
Predicate<T> notNull<T>() => not(isNull());
|
||||
|
||||
/// Expects a field to be less than a given [value].
|
||||
Predicate<T> lessThan<T>(T value) =>
|
||||
new Predicate<T>._(PredicateType.less, value);
|
||||
|
||||
/// Expects a field to be less than or equal to a given [value].
|
||||
Predicate<T> lessThanOrEqual<T>(T value) => lessThan(value) | equals(value);
|
||||
|
||||
/// Expects a field to be greater than a given [value].
|
||||
Predicate<T> greaterThan<T>(T value) =>
|
||||
new Predicate<T>._(PredicateType.greater, value);
|
||||
|
||||
/// Expects a field to be greater than or equal to a given [value].
|
||||
Predicate<T> greaterThanOrEqual<T>(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<String, Predicate> _fields = {};
|
||||
final Map<String, SortType> _sort = {};
|
||||
|
||||
/// Each field in a query is actually a [Predicate], and therefore acts as a contract
|
||||
/// with the underlying service.
|
||||
Map<String, Predicate> get fields =>
|
||||
new Map<String, Predicate>.unmodifiable(_fields);
|
||||
|
||||
/// The sorting order applied to this query.
|
||||
Map<String, SortType> get sorting =>
|
||||
new Map<String, SortType>.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;
|
||||
}
|
||||
|
||||
/// A mechanism used to express an expectation about some object ([target]).
|
||||
class Predicate<T> {
|
||||
/// The type of expectation we are declaring.
|
||||
final PredicateType type;
|
||||
|
||||
/// The single argument of this target.
|
||||
final T target;
|
||||
final Iterable<T> args;
|
||||
|
||||
Predicate._(this.type, this.target, [this.args]);
|
||||
|
||||
Predicate<T> operator &(Predicate<T> other) => and(other);
|
||||
|
||||
Predicate<T> operator |(Predicate<T> other) => or(other);
|
||||
|
||||
Predicate<T> and(Predicate<T> other) {
|
||||
return new MultiPredicate._(PredicateType.and, [this, other]);
|
||||
// Otherwise, add the next char, unless it's a null byte.
|
||||
else if ((ch = scanner.readChar()) != 0 && ch != null)
|
||||
buf.writeCharCode(ch);
|
||||
}
|
||||
|
||||
Predicate<T> or(Predicate<T> other) {
|
||||
return new MultiPredicate._(PredicateType.or, [this, other]);
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
abstract class SqlExpressionBuilder<T> {
|
||||
bool get hasValue;
|
||||
|
||||
String compile();
|
||||
|
||||
void isBetween(T lower, T upper);
|
||||
|
||||
void isNotBetween(T lower, T upper);
|
||||
|
||||
void isIn(Iterable<T> values);
|
||||
|
||||
void isNotIn(Iterable<T> values);
|
||||
}
|
||||
|
||||
class NumericSqlExpressionBuilder<T extends num>
|
||||
implements SqlExpressionBuilder<T> {
|
||||
bool _hasValue = false;
|
||||
String _op = '=';
|
||||
String _raw;
|
||||
T _value;
|
||||
|
||||
@override
|
||||
bool get hasValue => _hasValue;
|
||||
|
||||
bool _change(String op, T value) {
|
||||
_raw = null;
|
||||
_op = op;
|
||||
_value = value;
|
||||
return _hasValue = true;
|
||||
}
|
||||
|
||||
@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(T lower, T upper) {
|
||||
_raw = 'BETWEEN $lower AND $upper';
|
||||
_hasValue = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void isNotBetween(T lower, T upper) {
|
||||
_raw = 'NOT BETWEEN $lower AND $upper';
|
||||
_hasValue = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void isIn(Iterable<T> values) {
|
||||
_raw = 'IN (' + values.join(', ') + ')';
|
||||
_hasValue = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void isNotIn(Iterable<T> values) {
|
||||
_raw = 'NOT IN (' + values.join(', ') + ')';
|
||||
_hasValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// An advanced [Predicate] that performs an operation of multiple other predicates.
|
||||
class MultiPredicate<T> extends Predicate<T> {
|
||||
final Iterable<Predicate<T>> targets;
|
||||
class StringSqlExpressionBuilder implements SqlExpressionBuilder<String> {
|
||||
bool _hasValue = false;
|
||||
String _op = '=', _raw, _value;
|
||||
|
||||
MultiPredicate._(PredicateType type, this.targets) : super._(type, null);
|
||||
@override
|
||||
bool get hasValue => _hasValue;
|
||||
|
||||
/// Use [targets] instead.
|
||||
@deprecated
|
||||
T get target => throw new UnsupportedError(
|
||||
'IterablePredicate has no `target`. Use `targets` instead.');
|
||||
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(String lower, String upper) {
|
||||
var l = sanitizeExpression(lower), u = sanitizeExpression(upper);
|
||||
_raw = "BETWEEN '$l' AND '$u'";
|
||||
_hasValue = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void isNotBetween(String lower, String upper) {
|
||||
var l = sanitizeExpression(lower), u = sanitizeExpression(upper);
|
||||
_raw = "NOT BETWEEN '$l' AND '$u'";
|
||||
_hasValue = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void isIn(Iterable<String> values) {
|
||||
_raw = 'IN (' +
|
||||
values.map(sanitizeExpression).map((s) => "'$s'").join(', ') +
|
||||
')';
|
||||
_hasValue = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void isNotIn(Iterable<String> values) {
|
||||
_raw = 'NOT IN (' +
|
||||
values.map(sanitizeExpression).map((s) => "'$s'").join(', ') +
|
||||
')';
|
||||
_hasValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// The various types of predicate.
|
||||
enum PredicateType {
|
||||
equals,
|
||||
any,
|
||||
isIn,
|
||||
negate,
|
||||
and,
|
||||
or,
|
||||
less,
|
||||
greater,
|
||||
class BooleanSqlExpressionBuilder implements SqlExpressionBuilder<bool> {
|
||||
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(bool lower, bool upper) => throw new UnsupportedError(
|
||||
'Booleans do not support BETWEEN expressions.');
|
||||
|
||||
@override
|
||||
void isNotBetween(bool lower, bool upper) => isBetween(lower, upper);
|
||||
|
||||
@override
|
||||
void isIn(Iterable<bool> values) {
|
||||
_raw = 'IN (' + values.map((b) => b ? 'TRUE' : 'FALSE').join(', ') + ')';
|
||||
_hasValue = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void isNotIn(Iterable<bool> values) {
|
||||
_raw =
|
||||
'NOT IN (' + values.map((b) => b ? 'TRUE' : 'FALSE').join(', ') + ')';
|
||||
_hasValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// The various modes of sorting.
|
||||
enum SortType { ascending, descending }
|
||||
class DateTimeSqlExpressionBuilder implements SqlExpressionBuilder<DateTime> {
|
||||
final NumericSqlExpressionBuilder<int> year =
|
||||
new NumericSqlExpressionBuilder<int>(),
|
||||
month = new NumericSqlExpressionBuilder<int>(),
|
||||
day = new NumericSqlExpressionBuilder<int>(),
|
||||
hour = new NumericSqlExpressionBuilder<int>(),
|
||||
minute = new NumericSqlExpressionBuilder<int>(),
|
||||
second = new NumericSqlExpressionBuilder<int>();
|
||||
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 ? dateYmdHms.format(dt) : dateYmd.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(Iterable<DateTime> values) {
|
||||
_raw = '$columnName IN (' +
|
||||
values.map(dateYmdHms.format).map((s) => '$s').join(', ') +
|
||||
')';
|
||||
}
|
||||
|
||||
@override
|
||||
void isNotIn(Iterable<DateTime> values) {
|
||||
_raw = '$columnName NOT IN (' +
|
||||
values.map(dateYmdHms.format).map((s) => '$s').join(', ') +
|
||||
')';
|
||||
}
|
||||
|
||||
@override
|
||||
void isBetween(DateTime lower, DateTime upper) {
|
||||
var l = dateYmdHms.format(lower), u = dateYmdHms.format(upper);
|
||||
_raw = "$columnName BETWEEN '$l' and '$u'";
|
||||
}
|
||||
|
||||
@override
|
||||
void isNotBetween(DateTime lower, DateTime upper) {
|
||||
var l = dateYmdHms.format(lower), u = dateYmdHms.format(upper);
|
||||
_raw = "$columnName NOT BETWEEN '$l' and '$u'";
|
||||
}
|
||||
|
||||
@override
|
||||
String compile() {
|
||||
if (_raw?.isNotEmpty == true) return _raw;
|
||||
List<String> 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 ');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
abstract class RelationshipType {
|
||||
static const int HAS_MANY = 0;
|
||||
static const int HAS_ONE = 1;
|
||||
static const int BELONGS_TO = 2;
|
||||
static const int BELONGS_TO_MANY = 3;
|
||||
static const int hasMany = 0;
|
||||
static const int hasOne = 1;
|
||||
static const int belongsTo = 2;
|
||||
static const int belongsToMany = 3;
|
||||
}
|
||||
|
||||
class Relationship {
|
||||
|
@ -25,7 +25,7 @@ class HasMany extends Relationship {
|
|||
String foreignKey,
|
||||
String foreignTable,
|
||||
bool cascadeOnDelete: false})
|
||||
: super(RelationshipType.HAS_MANY,
|
||||
: super(RelationshipType.hasMany,
|
||||
localKey: localKey,
|
||||
foreignKey: foreignKey,
|
||||
foreignTable: foreignTable,
|
||||
|
@ -40,7 +40,7 @@ class HasOne extends Relationship {
|
|||
String foreignKey,
|
||||
String foreignTable,
|
||||
bool cascadeOnDelete: false})
|
||||
: super(RelationshipType.HAS_ONE,
|
||||
: super(RelationshipType.hasOne,
|
||||
localKey: localKey,
|
||||
foreignKey: foreignKey,
|
||||
foreignTable: foreignTable,
|
||||
|
@ -52,7 +52,7 @@ const HasOne hasOne = const HasOne();
|
|||
class BelongsTo extends Relationship {
|
||||
const BelongsTo(
|
||||
{String localKey: 'id', String foreignKey, String foreignTable})
|
||||
: super(RelationshipType.BELONGS_TO,
|
||||
: super(RelationshipType.belongsTo,
|
||||
localKey: localKey,
|
||||
foreignKey: foreignKey,
|
||||
foreignTable: foreignTable);
|
||||
|
@ -63,7 +63,7 @@ const BelongsTo belongsTo = const BelongsTo();
|
|||
class BelongsToMany extends Relationship {
|
||||
const BelongsToMany(
|
||||
{String localKey: 'id', String foreignKey, String foreignTable})
|
||||
: super(RelationshipType.BELONGS_TO_MANY,
|
||||
: super(RelationshipType.belongsToMany,
|
||||
localKey: localKey,
|
||||
foreignKey: foreignKey,
|
||||
foreignTable: foreignTable);
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
name: angel_orm
|
||||
version: 1.0.0-alpha+11
|
||||
version: 2.0.0-dev
|
||||
description: Runtime support for Angel's ORM.
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
homepage: https://github.com/angel-dart/orm
|
||||
environment:
|
||||
sdk: '>=2.0.0-dev.1.2 <2.0.0'
|
||||
sdk: '>=2.0.0-dev.1.2 <3.0.0'
|
||||
dependencies:
|
||||
angel_model: ^1.0.0
|
||||
intl: ^0.15.7
|
||||
meta: ^1.0.0
|
||||
string_scanner: ^1.0.0
|
||||
dev_dependencies:
|
||||
angel_model: ^1.0.0
|
|
@ -1,4 +0,0 @@
|
|||
export 'src/builder/orm/migration.dart';
|
||||
export 'src/builder/orm/postgres.dart';
|
||||
export 'src/builder/orm/service.dart';
|
||||
export 'src/builder/orm/sql_migration.dart';
|
|
@ -1,207 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'package:analyzer/dart/constant/value.dart';
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:analyzer/dart/element/type.dart';
|
||||
import 'package:analyzer/src/dart/element/element.dart';
|
||||
import 'package:angel_orm/angel_orm.dart';
|
||||
import 'package:angel_serialize_generator/build_context.dart' as serialize;
|
||||
import 'package:angel_serialize_generator/context.dart' as serialize;
|
||||
import 'package:build/build.dart';
|
||||
import 'package:inflection/inflection.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
import 'package:source_gen/source_gen.dart';
|
||||
import 'postgres_build_context.dart';
|
||||
|
||||
const TypeChecker columnTypeChecker = const TypeChecker.fromRuntime(Column),
|
||||
dateTimeTypeChecker = const TypeChecker.fromRuntime(DateTime),
|
||||
ormTypeChecker = const TypeChecker.fromRuntime(ORM),
|
||||
relationshipTypeChecker = const TypeChecker.fromRuntime(Relationship);
|
||||
|
||||
const TypeChecker hasOneTypeChecker = const TypeChecker.fromRuntime(HasOne),
|
||||
hasManyTypeChecker = const TypeChecker.fromRuntime(HasMany),
|
||||
belongsToTypeChecker = const TypeChecker.fromRuntime(BelongsTo),
|
||||
belongsToManyTypeChecker = const TypeChecker.fromRuntime(BelongsToMany);
|
||||
|
||||
ColumnType inferColumnType(DartType type) {
|
||||
if (const TypeChecker.fromRuntime(String).isAssignableFromType(type))
|
||||
return ColumnType.VAR_CHAR;
|
||||
if (const TypeChecker.fromRuntime(int).isAssignableFromType(type))
|
||||
return ColumnType.INT;
|
||||
if (const TypeChecker.fromRuntime(double).isAssignableFromType(type))
|
||||
return ColumnType.DECIMAL;
|
||||
if (const TypeChecker.fromRuntime(num).isAssignableFromType(type))
|
||||
return ColumnType.NUMERIC;
|
||||
if (const TypeChecker.fromRuntime(bool).isAssignableFromType(type))
|
||||
return ColumnType.BOOLEAN;
|
||||
if (const TypeChecker.fromRuntime(DateTime).isAssignableFromType(type))
|
||||
return ColumnType.TIME_STAMP;
|
||||
return null;
|
||||
}
|
||||
|
||||
Column reviveColumn(ConstantReader cr) {
|
||||
// TODO: Get index type, column type...
|
||||
var args = cr.revive().namedArguments;
|
||||
IndexType indexType = IndexType.NONE;
|
||||
ColumnType columnType;
|
||||
|
||||
if (args.containsKey('index')) {
|
||||
indexType = IndexType.values[args['index'].getField('index').toIntValue()];
|
||||
}
|
||||
|
||||
if (args.containsKey('type')) {
|
||||
columnType = new _ColumnType(args['type'].getField('name').toStringValue());
|
||||
}
|
||||
|
||||
return new Column(
|
||||
nullable: cr.peek('nullable')?.boolValue,
|
||||
length: cr.peek('length')?.intValue,
|
||||
defaultValue: cr.peek('defaultValue')?.literalValue,
|
||||
type: columnType,
|
||||
index: indexType,
|
||||
);
|
||||
}
|
||||
|
||||
ORM reviveOrm(ConstantReader cr) {
|
||||
return new ORM(cr.peek('tableName')?.stringValue);
|
||||
}
|
||||
|
||||
Relationship reviveRelationship(DartObject relationshipAnnotation) {
|
||||
var cr = new ConstantReader(relationshipAnnotation);
|
||||
var r = cr.revive().namedArguments;
|
||||
int type = -1;
|
||||
|
||||
if (cr.instanceOf(hasOneTypeChecker))
|
||||
type = RelationshipType.HAS_ONE;
|
||||
else if (cr.instanceOf(hasManyTypeChecker))
|
||||
type = RelationshipType.HAS_MANY;
|
||||
else if (cr.instanceOf(belongsToTypeChecker))
|
||||
type = RelationshipType.BELONGS_TO;
|
||||
else if (cr.instanceOf(belongsToManyTypeChecker))
|
||||
type = RelationshipType.BELONGS_TO_MANY;
|
||||
else
|
||||
throw new UnsupportedError(
|
||||
'Unsupported relationship type "${relationshipAnnotation.type.name}".');
|
||||
|
||||
return new Relationship(type,
|
||||
localKey: r['localKey']?.toStringValue(),
|
||||
foreignKey: r['foreignKey']?.toStringValue(),
|
||||
foreignTable: r['foreignTable']?.toStringValue(),
|
||||
cascadeOnDelete: r['cascadeOnDelete']?.toBoolValue());
|
||||
}
|
||||
|
||||
Future<PostgresBuildContext> buildContext(
|
||||
ClassElement clazz,
|
||||
ORM annotation,
|
||||
BuildStep buildStep,
|
||||
Resolver resolver,
|
||||
bool autoSnakeCaseNames,
|
||||
bool autoIdAndDateFields) async {
|
||||
var raw = await serialize.buildContext(clazz, null, buildStep, resolver,
|
||||
autoSnakeCaseNames != false, autoIdAndDateFields != false);
|
||||
var ctx = await PostgresBuildContext.create(
|
||||
clazz, raw, annotation, resolver, buildStep,
|
||||
tableName: (annotation.tableName?.isNotEmpty == true)
|
||||
? annotation.tableName
|
||||
: pluralize(new ReCase(clazz.name).snakeCase),
|
||||
autoSnakeCaseNames: autoSnakeCaseNames != false,
|
||||
autoIdAndDateFields: autoIdAndDateFields != false);
|
||||
List<String> fieldNames = [];
|
||||
List<FieldElement> fields = [];
|
||||
|
||||
for (var field in raw.fields) {
|
||||
fieldNames.add(field.name);
|
||||
|
||||
// Check for joins.
|
||||
var canJoins = canJoinTypeChecker.annotationsOf(field);
|
||||
|
||||
for (var ann in canJoins) {
|
||||
var cr = new ConstantReader(ann);
|
||||
ctx.joins[field.name] ??= [];
|
||||
ctx.joins[field.name].add(new JoinContext(
|
||||
resolveModelAncestor(cr.read('type').typeValue),
|
||||
cr.read('foreignKey').stringValue,
|
||||
));
|
||||
}
|
||||
|
||||
// Check for relationship. If so, skip.
|
||||
var relationshipAnnotation =
|
||||
relationshipTypeChecker.firstAnnotationOf(field);
|
||||
|
||||
if (relationshipAnnotation != null) {
|
||||
ctx.relationshipFields.add(field);
|
||||
ctx.relationships[field.name] =
|
||||
reviveRelationship(relationshipAnnotation);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for column annotation...
|
||||
Column column;
|
||||
var columnAnnotation = columnTypeChecker.firstAnnotationOf(field);
|
||||
|
||||
if (columnAnnotation != null) {
|
||||
column = reviveColumn(new ConstantReader(columnAnnotation));
|
||||
}
|
||||
|
||||
if (column == null && field.name == 'id' && ctx.shimmed['id'] == true) {
|
||||
column = const Column(type: ColumnType.SERIAL);
|
||||
}
|
||||
|
||||
if (column == null) {
|
||||
// Guess what kind of column this is...
|
||||
column = new Column(
|
||||
type: inferColumnType(
|
||||
field.type,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (column != null && column.type == null) {
|
||||
column = new Column(
|
||||
nullable: column.nullable,
|
||||
length: column.length,
|
||||
index: column.index,
|
||||
defaultValue: column.defaultValue,
|
||||
type: inferColumnType(field.type),
|
||||
);
|
||||
}
|
||||
|
||||
if (column?.type == null)
|
||||
throw 'Cannot infer SQL column type for field "${field.name}" with type "${field.type.name}".';
|
||||
ctx.columnInfo[field.name] = column;
|
||||
fields.add(field);
|
||||
}
|
||||
|
||||
ctx.fields.addAll(fields);
|
||||
|
||||
// Add belongs to fields
|
||||
// TODO: Do this for belongs to many as well
|
||||
ctx.relationships.forEach((name, r) {
|
||||
var relationship = ctx.populateRelationship(name);
|
||||
var rc = new ReCase(relationship.localKey);
|
||||
|
||||
if (relationship.type == RelationshipType.BELONGS_TO) {
|
||||
ctx.fields.removeWhere((f) => f.name == rc.camelCase);
|
||||
var field = new RelationshipConstraintField(
|
||||
rc.camelCase, ctx.typeProvider.intType, name);
|
||||
ctx.fields.add(field);
|
||||
ctx.aliases[field.name] = relationship.localKey;
|
||||
}
|
||||
});
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
class RelationshipConstraintField extends FieldElementImpl {
|
||||
@override
|
||||
final DartType type;
|
||||
final String originalName;
|
||||
RelationshipConstraintField(String name, this.type, this.originalName)
|
||||
: super(name, -1);
|
||||
}
|
||||
|
||||
class _ColumnType implements ColumnType {
|
||||
@override
|
||||
final String name;
|
||||
|
||||
_ColumnType(this.name);
|
||||
}
|
|
@ -1,206 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:angel_orm/angel_orm.dart';
|
||||
import 'package:build/build.dart';
|
||||
import 'package:code_builder/code_builder.dart';
|
||||
import 'package:source_gen/source_gen.dart' hide LibraryBuilder;
|
||||
import 'build_context.dart';
|
||||
import 'postgres_build_context.dart';
|
||||
import 'lib_core.dart' as lib$core;
|
||||
|
||||
class MigrationGenerator extends GeneratorForAnnotation<ORM> {
|
||||
static final Parameter _schemaParam = new Parameter((b) {
|
||||
b
|
||||
..name = 'schema'
|
||||
..type = new TypeReference((b) => b.symbol = 'Schema');
|
||||
});
|
||||
static final Expression _schema = new CodeExpression(new Code('schema'));
|
||||
|
||||
/// If `true` (default), then field names will automatically be (de)serialized as snake_case.
|
||||
final bool autoSnakeCaseNames;
|
||||
|
||||
/// If `true` (default), then the schema will automatically add id, created_at and updated_at fields.
|
||||
final bool autoIdAndDateFields;
|
||||
|
||||
const MigrationGenerator(
|
||||
{this.autoSnakeCaseNames: true, this.autoIdAndDateFields: true});
|
||||
|
||||
@override
|
||||
Future<String> generateForAnnotatedElement(
|
||||
Element element, ConstantReader annotation, BuildStep buildStep) async {
|
||||
if (buildStep.inputId.path.contains('.migration.g.dart')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (element is! ClassElement)
|
||||
throw 'Only classes can be annotated with @ORM().';
|
||||
var resolver = await buildStep.resolver;
|
||||
var ctx = await buildContext(element, reviveOrm(annotation), buildStep,
|
||||
resolver, autoSnakeCaseNames != false, autoIdAndDateFields != false);
|
||||
var lib = generateMigrationLibrary(ctx, element, resolver, buildStep);
|
||||
if (lib == null) return null;
|
||||
var emitter = new DartEmitter();
|
||||
return lib.accept(emitter).toString();
|
||||
}
|
||||
|
||||
Library generateMigrationLibrary(PostgresBuildContext ctx,
|
||||
ClassElement element, Resolver resolver, BuildStep buildStep) {
|
||||
return new Library((lib) {
|
||||
lib.directives.add([
|
||||
new Directive.import('package:angel_migration/angel_migration.dart'),
|
||||
]);
|
||||
|
||||
lib.body.add(new Class((b) {
|
||||
b.name = '${ctx.modelClassName}Migration';
|
||||
b.extend = new Reference('Migration');
|
||||
}));
|
||||
|
||||
lib.methods.add(buildUpMigration(ctx, lib));
|
||||
lib.methods.add(buildDownMigration(ctx));
|
||||
});
|
||||
}
|
||||
|
||||
Method buildUpMigration(PostgresBuildContext ctx, LibraryBuilder lib) {
|
||||
return new Method((meth) {
|
||||
meth.name = 'up';
|
||||
meth.annotations.add(lib$core.override);
|
||||
meth.requiredParameters.add(_schemaParam);
|
||||
|
||||
var closure = new Method((closure) {
|
||||
closure.requiredParameters.add(new Parameter((b) => b.name = 'table'));
|
||||
var table = new Reference('table');
|
||||
|
||||
List<String> dup = [];
|
||||
bool hasOrmImport = false;
|
||||
ctx.columnInfo.forEach((name, col) {
|
||||
var key = ctx.resolveFieldName(name);
|
||||
|
||||
if (dup.contains(key))
|
||||
return;
|
||||
else {
|
||||
if (key != 'id' || autoIdAndDateFields == false) {
|
||||
// Check for relationships that might duplicate
|
||||
for (var rName in ctx.relationships.keys) {
|
||||
var relationship = ctx.populateRelationship(rName);
|
||||
if (relationship.localKey == key) return;
|
||||
}
|
||||
}
|
||||
|
||||
dup.add(key);
|
||||
}
|
||||
|
||||
String methodName;
|
||||
List<Expression> positional = [literal(key)];
|
||||
Map<String, Expression> named = {};
|
||||
|
||||
if (autoIdAndDateFields != false && name == 'id') methodName = 'serial';
|
||||
|
||||
if (methodName == null) {
|
||||
switch (col.type) {
|
||||
case ColumnType.VAR_CHAR:
|
||||
methodName = 'varchar';
|
||||
if (col.length != null) named['length'] = literal(col.length);
|
||||
break;
|
||||
case ColumnType.SERIAL:
|
||||
methodName = 'serial';
|
||||
break;
|
||||
case ColumnType.INT:
|
||||
methodName = 'integer';
|
||||
break;
|
||||
case ColumnType.FLOAT:
|
||||
methodName = 'float';
|
||||
break;
|
||||
case ColumnType.NUMERIC:
|
||||
methodName = 'numeric';
|
||||
break;
|
||||
case ColumnType.BOOLEAN:
|
||||
methodName = 'boolean';
|
||||
break;
|
||||
case ColumnType.DATE:
|
||||
methodName = 'date';
|
||||
break;
|
||||
case ColumnType.DATE_TIME:
|
||||
methodName = 'dateTime';
|
||||
break;
|
||||
case ColumnType.TIME_STAMP:
|
||||
methodName = 'timeStamp';
|
||||
break;
|
||||
default:
|
||||
if (!hasOrmImport) {
|
||||
hasOrmImport = true;
|
||||
lib.directives.add(new Directive.import('package:angel_orm/angel_orm.dart'));
|
||||
}
|
||||
|
||||
Expression provColumn;
|
||||
|
||||
if (col.length == null) {
|
||||
methodName = 'declare';
|
||||
provColumn = new CodeExpression(new Code("new ColumnType('${col.type.name}')"));
|
||||
} else {
|
||||
methodName = 'declareColumn';
|
||||
provColumn = new CodeExpression(new Code("new Column({type: new Column('${col.type.name}'), length: ${col.length})"));
|
||||
}
|
||||
|
||||
positional.add(provColumn);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var field = table.property(methodName).call(positional, named);
|
||||
var cascade = <Expression Function(Expression)>[];
|
||||
|
||||
if (col.defaultValue != null) {
|
||||
cascade
|
||||
.add((e) => e.property('defaultsTo').call([literal(col.defaultValue)]));
|
||||
}
|
||||
|
||||
if (col.index == IndexType.PRIMARY_KEY ||
|
||||
(autoIdAndDateFields != false && name == 'id'))
|
||||
cascade.add((e) => e.property('primaryKey').call([]));
|
||||
else if (col.index == IndexType.UNIQUE)
|
||||
cascade.add((e) => e.property('unique').call([]));
|
||||
|
||||
if (col.nullable != true) cascade.add((e) => e.property('notNull').call([]));
|
||||
|
||||
field = cascade.isEmpty
|
||||
? field
|
||||
: field.cascade((e) => cascade.map((f) => f(e)).toList());
|
||||
closure.addStatement(field);
|
||||
});
|
||||
|
||||
ctx.relationships.forEach((name, r) {
|
||||
var relationship = ctx.populateRelationship(name);
|
||||
|
||||
if (relationship.isBelongsTo) {
|
||||
var key = relationship.localKey;
|
||||
|
||||
var field = table.property('integer').call([literal(key)]);
|
||||
// .references('user', 'id').onDeleteCascade()
|
||||
var ref = field.property('references').call([
|
||||
literal(relationship.foreignTable),
|
||||
literal(relationship.foreignKey),
|
||||
]);
|
||||
|
||||
if (relationship.cascadeOnDelete != false && relationship.isSingular)
|
||||
ref = ref.property('onDeleteCascade').call([]);
|
||||
return closure.addStatement(ref);
|
||||
}
|
||||
});
|
||||
|
||||
meth.addStatement(_schema.property('create').call([
|
||||
literal(ctx.tableName),
|
||||
closure,
|
||||
]));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Method buildDownMigration(PostgresBuildContext ctx) {
|
||||
return new Method((b) {
|
||||
b.name = 'down';
|
||||
b.requiredParameters.add(_schemaParam);
|
||||
b.annotations.add(lib$core.override);
|
||||
b.body.add(new Code("schema.drop('${ctx.tableName}')"));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,318 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'package:analyzer/dart/constant/value.dart';
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:analyzer/dart/element/type.dart';
|
||||
import 'package:analyzer/src/generated/resolver.dart';
|
||||
import 'package:angel_model/angel_model.dart';
|
||||
import 'package:angel_orm/angel_orm.dart';
|
||||
import 'package:angel_serialize_generator/context.dart';
|
||||
import 'package:build/build.dart';
|
||||
import 'package:code_builder/code_builder.dart';
|
||||
import 'package:inflection/inflection.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
import 'package:source_gen/source_gen.dart';
|
||||
import 'build_context.dart';
|
||||
|
||||
const TypeChecker canJoinTypeChecker = const TypeChecker.fromRuntime(CanJoin);
|
||||
|
||||
DartType resolveModelAncestor(DartType type) {
|
||||
DartType refType = type;
|
||||
|
||||
while (refType != null) {
|
||||
if (!const TypeChecker.fromRuntime(Model).isAssignableFromType(refType)) {
|
||||
var parent = (refType.element as ClassElement).allSupertypes[0];
|
||||
if (parent != refType)
|
||||
refType = parent;
|
||||
else
|
||||
refType = null;
|
||||
} else
|
||||
break;
|
||||
}
|
||||
|
||||
if (refType != null) return refType;
|
||||
|
||||
throw '${type.name} does not extend Model.';
|
||||
}
|
||||
|
||||
class JoinContext {
|
||||
final DartType type;
|
||||
final String foreignKey;
|
||||
JoinContext(this.type, this.foreignKey);
|
||||
}
|
||||
|
||||
class PostgresBuildContext extends BuildContext {
|
||||
LibraryElement _libraryCache;
|
||||
TypeProvider _typeProviderCache;
|
||||
TypeBuilder _modelClassBuilder,
|
||||
_queryClassBuilder,
|
||||
_whereClassBuilder,
|
||||
_postgresqlConnectionBuilder;
|
||||
String _prefix;
|
||||
final Map<String, Relationship> _populatedRelationships = {};
|
||||
final Map<String, Column> columnInfo = {};
|
||||
final Map<String, IndexType> indices = {};
|
||||
final Map<String, List<JoinContext>> joins = {};
|
||||
final Map<String, Relationship> relationships = {};
|
||||
final bool autoSnakeCaseNames, autoIdAndDateFields;
|
||||
final String tableName;
|
||||
final ORM ormAnnotation;
|
||||
final ClassElement element;
|
||||
final BuildContext raw;
|
||||
final Resolver resolver;
|
||||
final BuildStep buildStep;
|
||||
ReCase _reCase;
|
||||
String primaryKeyName = 'id';
|
||||
|
||||
PostgresBuildContext._(
|
||||
this.element, this.raw, this.ormAnnotation, this.resolver, this.buildStep,
|
||||
{this.tableName, this.autoSnakeCaseNames, this.autoIdAndDateFields})
|
||||
: super(raw.annotation,
|
||||
originalClassName: raw.originalClassName,
|
||||
sourceFilename: raw.sourceFilename);
|
||||
|
||||
static Future<PostgresBuildContext> create(
|
||||
ClassElement element,
|
||||
BuildContext raw,
|
||||
ORM ormAnnotation,
|
||||
Resolver resolver,
|
||||
BuildStep buildStep,
|
||||
{String tableName,
|
||||
bool autoSnakeCaseNames,
|
||||
bool autoIdAndDateFields}) async {
|
||||
var ctx = new PostgresBuildContext._(
|
||||
element,
|
||||
raw,
|
||||
ormAnnotation,
|
||||
resolver,
|
||||
buildStep,
|
||||
tableName: tableName,
|
||||
autoSnakeCaseNames: autoSnakeCaseNames,
|
||||
autoIdAndDateFields: autoIdAndDateFields,
|
||||
);
|
||||
|
||||
// Library
|
||||
ctx._libraryCache = await resolver.libraryFor(buildStep.inputId);
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
final List<FieldElement> fields = [], relationshipFields = [];
|
||||
|
||||
ReCase get reCase => _reCase ?? new ReCase(modelClassName);
|
||||
|
||||
TypeBuilder get modelClassBuilder =>
|
||||
_modelClassBuilder ??= new TypeBuilder(modelClassName);
|
||||
|
||||
TypeBuilder get queryClassBuilder =>
|
||||
_queryClassBuilder ??= new TypeBuilder(queryClassName);
|
||||
|
||||
TypeBuilder get whereClassBuilder =>
|
||||
_whereClassBuilder ??= new TypeBuilder(whereClassName);
|
||||
|
||||
TypeBuilder get postgreSQLConnectionBuilder =>
|
||||
_postgresqlConnectionBuilder ??= new TypeBuilder('PostgreSQLConnection');
|
||||
|
||||
String get prefix {
|
||||
if (_prefix != null) return _prefix;
|
||||
if (relationships.isEmpty)
|
||||
return _prefix = '';
|
||||
else
|
||||
return _prefix = tableName + '.';
|
||||
}
|
||||
|
||||
Map<String, String> get aliases => raw.aliases;
|
||||
|
||||
Map<String, bool> get shimmed => raw.shimmed;
|
||||
|
||||
String get sourceFilename => raw.sourceFilename;
|
||||
|
||||
String get modelClassName => raw.modelClassName;
|
||||
|
||||
String get originalClassName => raw.originalClassName;
|
||||
|
||||
String get queryClassName => modelClassName + 'Query';
|
||||
String get whereClassName => queryClassName + 'Where';
|
||||
|
||||
LibraryElement get library => _libraryCache;
|
||||
|
||||
TypeProvider get typeProvider =>
|
||||
_typeProviderCache ??= library.context.typeProvider;
|
||||
|
||||
FieldElement resolveRelationshipField(String name) =>
|
||||
relationshipFields.firstWhere((f) => f.name == name, orElse: () => null);
|
||||
|
||||
PopulatedRelationship populateRelationship(String name) {
|
||||
return _populatedRelationships.putIfAbsent(name, () {
|
||||
var f = raw.fields.firstWhere((f) => f.name == name);
|
||||
var relationship = relationships[name];
|
||||
DartType refType = f.type;
|
||||
|
||||
if (refType.isAssignableTo(typeProvider.listType) ||
|
||||
refType.name == 'List') {
|
||||
var iType = refType as InterfaceType;
|
||||
|
||||
if (iType.typeArguments.isEmpty)
|
||||
throw 'Relationship "${f.name}" cannot be modeled as a generic List.';
|
||||
|
||||
refType = iType.typeArguments.first;
|
||||
}
|
||||
|
||||
var typeName = refType.name.startsWith('_')
|
||||
? refType.name.substring(1)
|
||||
: refType.name;
|
||||
var rc = new ReCase(typeName);
|
||||
|
||||
if (relationship.type == RelationshipType.HAS_ONE ||
|
||||
relationship.type == RelationshipType.HAS_MANY) {
|
||||
//print('Has many $tableName');
|
||||
var single = singularize(tableName);
|
||||
var foreignKey = relationship.foreignTable ??
|
||||
(autoSnakeCaseNames != false ? '${single}_id' : '${single}Id');
|
||||
var localKey = relationship.localKey ?? 'id';
|
||||
var foreignTable = relationship.foreignTable ??
|
||||
(autoSnakeCaseNames != false
|
||||
? pluralize(rc.snakeCase)
|
||||
: pluralize(typeName));
|
||||
return new PopulatedRelationship(
|
||||
relationship.type,
|
||||
f.name,
|
||||
f.type,
|
||||
buildStep,
|
||||
resolver,
|
||||
autoSnakeCaseNames,
|
||||
autoIdAndDateFields,
|
||||
relationship.type == RelationshipType.HAS_ONE,
|
||||
typeProvider,
|
||||
localKey: localKey,
|
||||
foreignKey: foreignKey,
|
||||
foreignTable: foreignTable,
|
||||
cascadeOnDelete: relationship.cascadeOnDelete);
|
||||
} else if (relationship.type == RelationshipType.BELONGS_TO ||
|
||||
relationship.type == RelationshipType.BELONGS_TO_MANY) {
|
||||
var localKey = relationship.localKey ??
|
||||
(autoSnakeCaseNames != false
|
||||
? '${rc.snakeCase}_id'
|
||||
: '${typeName}Id');
|
||||
var foreignKey = relationship.foreignKey ?? 'id';
|
||||
var foreignTable = relationship.foreignTable ??
|
||||
(autoSnakeCaseNames != false
|
||||
? pluralize(rc.snakeCase)
|
||||
: pluralize(typeName));
|
||||
return new PopulatedRelationship(
|
||||
relationship.type,
|
||||
f.name,
|
||||
f.type,
|
||||
buildStep,
|
||||
resolver,
|
||||
autoSnakeCaseNames,
|
||||
autoIdAndDateFields,
|
||||
relationship.type == RelationshipType.BELONGS_TO,
|
||||
typeProvider,
|
||||
localKey: localKey,
|
||||
foreignKey: foreignKey,
|
||||
foreignTable: foreignTable,
|
||||
cascadeOnDelete: relationship.cascadeOnDelete);
|
||||
} else
|
||||
throw new UnsupportedError(
|
||||
'Invalid relationship type: ${relationship.type}');
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PostgresBuildContext: $originalClassName';
|
||||
}
|
||||
}
|
||||
|
||||
class PopulatedRelationship extends Relationship {
|
||||
bool _isList;
|
||||
DartType _modelType;
|
||||
PostgresBuildContext _modelTypeContext;
|
||||
DartObject _modelTypeORM;
|
||||
final String originalName;
|
||||
final DartType dartType;
|
||||
final BuildStep buildStep;
|
||||
final Resolver resolver;
|
||||
final bool autoSnakeCaseNames, autoIdAndDateFields;
|
||||
final bool isSingular;
|
||||
final TypeProvider typeProvider;
|
||||
|
||||
PopulatedRelationship(
|
||||
int type,
|
||||
this.originalName,
|
||||
this.dartType,
|
||||
this.buildStep,
|
||||
this.resolver,
|
||||
this.autoSnakeCaseNames,
|
||||
this.autoIdAndDateFields,
|
||||
this.isSingular,
|
||||
this.typeProvider,
|
||||
{String localKey,
|
||||
String foreignKey,
|
||||
String foreignTable,
|
||||
bool cascadeOnDelete})
|
||||
: super(type,
|
||||
localKey: localKey,
|
||||
foreignKey: foreignKey,
|
||||
foreignTable: foreignTable,
|
||||
cascadeOnDelete: cascadeOnDelete);
|
||||
|
||||
bool get isBelongsTo =>
|
||||
type == RelationshipType.BELONGS_TO ||
|
||||
type == RelationshipType.BELONGS_TO_MANY;
|
||||
|
||||
bool get isHas =>
|
||||
type == RelationshipType.HAS_ONE || type == RelationshipType.HAS_MANY;
|
||||
|
||||
bool get isList => _isList ??=
|
||||
dartType.isAssignableTo(typeProvider.listType) || dartType.name == 'List';
|
||||
|
||||
DartType get modelType {
|
||||
if (_modelType != null) return _modelType;
|
||||
DartType searchType = dartType;
|
||||
var ormChecker = new TypeChecker.fromRuntime(ORM);
|
||||
|
||||
// Get inner type from List if any...
|
||||
if (!isSingular) {
|
||||
if (!isList)
|
||||
throw '"$originalName" is a many-to-one relationship, and thus it should be represented as a List within your Dart class. You have it represented as ${dartType.name}.';
|
||||
else {
|
||||
var iType = dartType as InterfaceType;
|
||||
if (iType.typeArguments.isEmpty)
|
||||
throw '"$originalName" is a many-to-one relationship, and should be modeled as a List that references another model type. Example: `List<T>`, where T is a model type.';
|
||||
else
|
||||
searchType = iType.typeArguments.first;
|
||||
}
|
||||
}
|
||||
|
||||
while (searchType != null) {
|
||||
var classElement = searchType.element as ClassElement;
|
||||
var ormAnnotation = ormChecker.firstAnnotationOf(classElement);
|
||||
|
||||
if (ormAnnotation != null) {
|
||||
_modelTypeORM = ormAnnotation;
|
||||
return _modelType = searchType;
|
||||
} else {
|
||||
// If we didn't find an @ORM(), then refer to the parent type.
|
||||
searchType = classElement.supertype;
|
||||
}
|
||||
}
|
||||
|
||||
throw new StateError(
|
||||
'Neither ${dartType.name} nor its parent types are annotated with an @ORM() annotation. It is impossible to compute this relationship.');
|
||||
}
|
||||
|
||||
Future<PostgresBuildContext> get modelTypeContext async {
|
||||
if (_modelTypeContext != null) return _modelTypeContext;
|
||||
var reader = new ConstantReader(_modelTypeORM);
|
||||
if (reader.isNull)
|
||||
reader = null;
|
||||
else
|
||||
reader = reader.read('tableName');
|
||||
var orm = reader == null
|
||||
? new ORM()
|
||||
: new ORM(reader.isString ? reader.stringValue : null);
|
||||
return _modelTypeContext = await buildContext(modelType.element, orm,
|
||||
buildStep, resolver, autoSnakeCaseNames, autoIdAndDateFields);
|
||||
}
|
||||
}
|
|
@ -1,399 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'package:analyzer/dart/ast/ast.dart';
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:angel_orm/angel_orm.dart';
|
||||
import 'package:build/build.dart';
|
||||
import 'package:code_builder/code_builder.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:recase/recase.dart';
|
||||
import 'package:source_gen/source_gen.dart' hide LibraryBuilder;
|
||||
import 'build_context.dart';
|
||||
import 'postgres_build_context.dart';
|
||||
|
||||
class PostgresServiceGenerator extends GeneratorForAnnotation<ORM> {
|
||||
static const List<TypeChecker> primitives = const [
|
||||
const TypeChecker.fromRuntime(String),
|
||||
const TypeChecker.fromRuntime(int),
|
||||
const TypeChecker.fromRuntime(bool),
|
||||
const TypeChecker.fromRuntime(double),
|
||||
const TypeChecker.fromRuntime(num),
|
||||
];
|
||||
|
||||
static final ExpressionBuilder id = reference('id'),
|
||||
params = reference('params'),
|
||||
connection = reference('connection'),
|
||||
query = reference('query'),
|
||||
buildQuery = reference('buildQuery'),
|
||||
applyData = reference('applyData'),
|
||||
where = reference('query').property('where'),
|
||||
toId = reference('toId'),
|
||||
data = reference('data');
|
||||
|
||||
final bool autoSnakeCaseNames;
|
||||
|
||||
final bool autoIdAndDateFields;
|
||||
|
||||
const PostgresServiceGenerator(
|
||||
{this.autoSnakeCaseNames: true, this.autoIdAndDateFields: true});
|
||||
|
||||
@override
|
||||
Future<String> generateForAnnotatedElement(
|
||||
Element element, ConstantReader annotation, BuildStep buildStep) async {
|
||||
if (buildStep.inputId.path.contains('.service.g.dart')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (element is! ClassElement)
|
||||
throw 'Only classes can be annotated with @ORM().';
|
||||
var resolver = await buildStep.resolver;
|
||||
var lib = await generateOrmLibrary(element.library, resolver, buildStep)
|
||||
.then((l) => l.buildAst());
|
||||
if (lib == null) return null;
|
||||
return prettyToSource(lib);
|
||||
}
|
||||
|
||||
Future<LibraryBuilder> generateOrmLibrary(LibraryElement libraryElement,
|
||||
Resolver resolver, BuildStep buildStep) async {
|
||||
var lib = new LibraryBuilder();
|
||||
lib.addDirective(new ImportBuilder('dart:async'));
|
||||
lib.addDirective(
|
||||
new ImportBuilder('package:angel_framework/angel_framework.dart'));
|
||||
lib.addDirective(new ImportBuilder('package:postgres/postgres.dart'));
|
||||
lib.addDirective(new ImportBuilder(p.basename(buildStep.inputId.path)));
|
||||
|
||||
var pathName = p.basenameWithoutExtension(
|
||||
p.basenameWithoutExtension(buildStep.inputId.path));
|
||||
lib.addDirective(new ImportBuilder('$pathName.orm.g.dart'));
|
||||
|
||||
var elements = libraryElement.definingCompilationUnit.unit.declarations
|
||||
.where((el) => el is ClassDeclaration);
|
||||
Map<ClassElement, PostgresBuildContext> contexts = {};
|
||||
List<String> done = [];
|
||||
|
||||
for (ClassDeclaration element in elements) {
|
||||
if (!done.contains(element.name)) {
|
||||
var ann = ormTypeChecker.firstAnnotationOf(element.element);
|
||||
if (ann != null) {
|
||||
contexts[element.element] = await buildContext(
|
||||
element.element,
|
||||
reviveOrm(new ConstantReader(ann)),
|
||||
buildStep,
|
||||
resolver,
|
||||
autoSnakeCaseNames != false,
|
||||
autoIdAndDateFields != false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (contexts.isEmpty) return null;
|
||||
|
||||
done.clear();
|
||||
for (var element in contexts.keys) {
|
||||
if (!done.contains(element.name)) {
|
||||
var ctx = contexts[element];
|
||||
lib.addMember(buildServiceClass(ctx));
|
||||
done.add(element.name);
|
||||
}
|
||||
}
|
||||
return lib;
|
||||
}
|
||||
|
||||
ClassBuilder buildServiceClass(PostgresBuildContext ctx) {
|
||||
var rc = new ReCase(ctx.modelClassName);
|
||||
var clazz = new ClassBuilder('${rc.pascalCase}Service',
|
||||
asExtends: new TypeBuilder('Service'));
|
||||
|
||||
// Add fields
|
||||
// connection, allowRemoveAll, allowQuery
|
||||
|
||||
clazz
|
||||
..addField(varFinal('connection', type: ctx.postgreSQLConnectionBuilder))
|
||||
..addField(varFinal('allowRemoveAll', type: lib$core.bool))
|
||||
..addField(varFinal('allowQuery', type: lib$core.bool));
|
||||
|
||||
clazz.addConstructor(constructor([
|
||||
thisField(parameter('connection')),
|
||||
thisField(named(parameter('allowRemoveAll', [literal(false)]))),
|
||||
thisField(named(parameter('allowQuery', [literal(false)])))
|
||||
]));
|
||||
|
||||
clazz.addMethod(buildQueryMethod(ctx));
|
||||
clazz.addMethod(buildToIdMethod(ctx));
|
||||
clazz.addMethod(buildApplyDataMethod(ctx));
|
||||
|
||||
clazz.addMethod(buildIndexMethod(ctx));
|
||||
clazz.addMethod(buildCreateMethod(ctx));
|
||||
clazz.addMethod(buildReadOrDeleteMethod('read', 'get', ctx));
|
||||
clazz.addMethod(buildReadOrDeleteMethod('remove', 'delete', ctx));
|
||||
clazz.addMethod(buildUpdateMethod(ctx));
|
||||
clazz.addMethod(buildModifyMethod(ctx));
|
||||
|
||||
return clazz;
|
||||
}
|
||||
|
||||
MethodBuilder buildQueryMethod(PostgresBuildContext ctx) {
|
||||
var meth =
|
||||
new MethodBuilder('buildQuery', returnType: ctx.queryClassBuilder)
|
||||
..addPositional(parameter('params', [lib$core.Map]));
|
||||
var paramQuery = params[literal('query')];
|
||||
meth.addStatement(
|
||||
varField('query', value: ctx.queryClassBuilder.newInstance([])));
|
||||
var ifStmt = ifThen(paramQuery.isInstanceOf(lib$core.Map));
|
||||
|
||||
ctx.fields.forEach((f) {
|
||||
var alias = ctx.resolveFieldName(f.name);
|
||||
var queryKey = paramQuery[literal(alias)];
|
||||
|
||||
if (f.type.isDynamic ||
|
||||
f.type.isObject ||
|
||||
f.type.isObject ||
|
||||
primitives.any((t) => t.isAssignableFromType(f.type))) {
|
||||
ifStmt
|
||||
.addStatement(where.property(f.name).invoke('equals', [queryKey]));
|
||||
} else if (dateTimeTypeChecker.isAssignableFromType(f.type)) {
|
||||
var dt = queryKey
|
||||
.isInstanceOf(lib$core.String)
|
||||
.ternary(lib$core.DateTime.invoke('parse', [queryKey]), queryKey);
|
||||
ifStmt.addStatement(
|
||||
where.property(f.name).invoke('equals', [updatedAt(dt)]));
|
||||
} else {
|
||||
print(
|
||||
'Cannot compute service query binding for field "${f.name}" in ${ctx.originalClassName}');
|
||||
}
|
||||
});
|
||||
|
||||
meth.addStatement(ifStmt);
|
||||
meth.addStatement(query.asReturn());
|
||||
return meth;
|
||||
}
|
||||
|
||||
MethodBuilder buildToIdMethod(PostgresBuildContext ctx) {
|
||||
var meth = new MethodBuilder('toId', returnType: lib$core.int)
|
||||
..addPositional(parameter('id'));
|
||||
|
||||
meth.addStatement(ifThen(id.isInstanceOf(lib$core.int), [
|
||||
id.asReturn(),
|
||||
elseThen([
|
||||
ifThen(id.equals(literal('null')).or(id.equals(literal(null))), [
|
||||
literal(null).asReturn(),
|
||||
elseThen([
|
||||
lib$core.int.invoke('parse', [id.invoke('toString', [])]).asReturn()
|
||||
])
|
||||
])
|
||||
])
|
||||
]));
|
||||
|
||||
return meth;
|
||||
}
|
||||
|
||||
MethodBuilder buildIndexMethod(PostgresBuildContext ctx) {
|
||||
// Future<List<T>> index([p]) => buildQuery(p).get(connection).toList();
|
||||
return method('index', [
|
||||
new TypeBuilder('Future', genericTypes: [
|
||||
new TypeBuilder('List', genericTypes: [ctx.modelClassBuilder])
|
||||
]),
|
||||
parameter('params', [lib$core.Map]).asOptional(),
|
||||
reference('buildQuery').call([params]).invoke('get', [connection]).invoke(
|
||||
'toList',
|
||||
[],
|
||||
).asReturn(),
|
||||
]);
|
||||
}
|
||||
|
||||
MethodBuilder buildReadOrDeleteMethod(
|
||||
String name, String operation, PostgresBuildContext ctx) {
|
||||
var throw404 = new MethodBuilder.closure()..addPositional(parameter('_'));
|
||||
throw404.addStatement(new TypeBuilder('AngelHttpException').newInstance(
|
||||
[],
|
||||
constructor: 'notFound',
|
||||
named: {
|
||||
'message':
|
||||
literal('No record found for ID ') + id.invoke('toString', []),
|
||||
},
|
||||
));
|
||||
|
||||
return method(name, [
|
||||
new TypeBuilder('Future', genericTypes: [ctx.modelClassBuilder]),
|
||||
parameter('id'),
|
||||
parameter('params', [lib$core.Map]).asOptional(),
|
||||
varField('query', value: buildQuery.call([params])),
|
||||
where.property('id').invoke('equals', [
|
||||
toId.call([id])
|
||||
]),
|
||||
query
|
||||
.invoke(operation, [connection])
|
||||
.property('first')
|
||||
.invoke('catchError', [
|
||||
throw404,
|
||||
])
|
||||
.asReturn(),
|
||||
]);
|
||||
}
|
||||
|
||||
MethodBuilder buildApplyDataMethod(PostgresBuildContext ctx) {
|
||||
var meth =
|
||||
new MethodBuilder('applyData', returnType: ctx.modelClassBuilder);
|
||||
meth.addPositional(parameter('data'));
|
||||
|
||||
meth.addStatement(ifThen(
|
||||
data.isInstanceOf(ctx.modelClassBuilder).or(data.equals(literal(null))),
|
||||
[
|
||||
data.asReturn(),
|
||||
],
|
||||
));
|
||||
|
||||
var ifStmt = new IfStatementBuilder(data.isInstanceOf(lib$core.Map));
|
||||
ifStmt.addStatement(
|
||||
varField('query', value: ctx.modelClassBuilder.newInstance([])));
|
||||
|
||||
applyFieldsToInstance(ctx, query, ifStmt.addStatement);
|
||||
|
||||
ifStmt.addStatement(query.asReturn());
|
||||
|
||||
ifStmt.setElse(
|
||||
new TypeBuilder('AngelHttpException')
|
||||
.newInstance([],
|
||||
constructor: 'badRequest',
|
||||
named: {'message': literal('Invalid data.')})
|
||||
.asThrow(),
|
||||
);
|
||||
|
||||
meth.addStatement(ifStmt);
|
||||
|
||||
return meth;
|
||||
}
|
||||
|
||||
MethodBuilder buildCreateMethod(PostgresBuildContext ctx) {
|
||||
var meth = new MethodBuilder('create',
|
||||
returnType:
|
||||
new TypeBuilder('Future', genericTypes: [ctx.modelClassBuilder]));
|
||||
meth
|
||||
..addPositional(parameter('data'))
|
||||
..addPositional(parameter('params', [lib$core.Map]).asOptional());
|
||||
|
||||
var rc = new ReCase(ctx.modelClassName);
|
||||
meth.addStatement(
|
||||
ctx.queryClassBuilder.invoke('insert${rc.pascalCase}', [
|
||||
connection,
|
||||
applyData.call([data])
|
||||
]).asReturn(),
|
||||
);
|
||||
|
||||
return meth;
|
||||
}
|
||||
|
||||
MethodBuilder buildModifyMethod(PostgresBuildContext ctx) {
|
||||
var meth = new MethodBuilder('modify',
|
||||
modifier: MethodModifier.asAsync,
|
||||
returnType:
|
||||
new TypeBuilder('Future', genericTypes: [ctx.modelClassBuilder]));
|
||||
meth
|
||||
..addPositional(parameter('id'))
|
||||
..addPositional(parameter('data'))
|
||||
..addPositional(parameter('params', [lib$core.Map]).asOptional());
|
||||
|
||||
// read() by id
|
||||
meth.addStatement(varField(
|
||||
'query',
|
||||
value: reference('read').call(
|
||||
[
|
||||
toId.call([id]),
|
||||
params
|
||||
],
|
||||
).asAwait(),
|
||||
));
|
||||
|
||||
var rc = new ReCase(ctx.modelClassName);
|
||||
|
||||
meth.addStatement(ifThen(data.isInstanceOf(ctx.modelClassBuilder), [
|
||||
data.asAssign(query),
|
||||
]));
|
||||
|
||||
var ifStmt = ifThen(data.isInstanceOf(lib$core.Map));
|
||||
|
||||
applyFieldsToInstance(ctx, query, ifStmt.addStatement);
|
||||
meth.addStatement(ifStmt);
|
||||
meth.addStatement(
|
||||
ctx.queryClassBuilder
|
||||
.invoke('update${rc.pascalCase}', [connection, query])
|
||||
.asAwait()
|
||||
.asReturn(),
|
||||
);
|
||||
|
||||
return meth;
|
||||
}
|
||||
|
||||
MethodBuilder buildUpdateMethod(PostgresBuildContext ctx) {
|
||||
var meth = new MethodBuilder('update',
|
||||
returnType:
|
||||
new TypeBuilder('Future', genericTypes: [ctx.modelClassBuilder]));
|
||||
meth
|
||||
..addPositional(parameter('id'))
|
||||
..addPositional(parameter('data'))
|
||||
..addPositional(parameter('params', [lib$core.Map]).asOptional());
|
||||
|
||||
var rc = new ReCase(ctx.modelClassName);
|
||||
meth.addStatement(
|
||||
ctx.queryClassBuilder.invoke('update${rc.pascalCase}', [
|
||||
connection,
|
||||
applyData.call([data])
|
||||
]).asReturn(),
|
||||
);
|
||||
|
||||
return meth;
|
||||
}
|
||||
|
||||
void parseParams(MethodBuilder meth, PostgresBuildContext ctx, {bool id}) {
|
||||
meth.addStatement(varField('query',
|
||||
value: buildQuery.call([
|
||||
reference('params')
|
||||
.notEquals(literal(null))
|
||||
.ternary(reference('params'), map({}))
|
||||
])));
|
||||
|
||||
if (id == true) {
|
||||
meth.addStatement(
|
||||
reference('query').property('where').property('id').invoke('equals', [
|
||||
reference('toId').call([reference('id')])
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
void applyFieldsToInstance(PostgresBuildContext ctx, ExpressionBuilder query,
|
||||
void addStatement(StatementBuilder statement)) {
|
||||
ctx.fields.forEach((f) {
|
||||
var alias = ctx.resolveFieldName(f.name);
|
||||
var dataKey = data[literal(alias)];
|
||||
ExpressionBuilder target;
|
||||
|
||||
// Skip `id`
|
||||
if (autoIdAndDateFields != false && f.name == 'id') return;
|
||||
|
||||
if (f.type.isDynamic ||
|
||||
f.type.isObject ||
|
||||
primitives.any((t) => t.isAssignableFromType(f.type))) {
|
||||
target = dataKey;
|
||||
} else if (dateTimeTypeChecker.isAssignableFromType(f.type)) {
|
||||
var dt = dataKey
|
||||
.isInstanceOf(lib$core.String)
|
||||
.ternary(lib$core.DateTime.invoke('parse', [dataKey]), dataKey);
|
||||
target = updatedAt(dt);
|
||||
} else {
|
||||
print(
|
||||
'Cannot compute service applyData() binding for field "${f.name}" in ${ctx.originalClassName}');
|
||||
}
|
||||
|
||||
if (target != null) {
|
||||
addStatement(ifThen(data.invoke('containsKey', [literal(alias)]),
|
||||
[target.asAssign(query.property(f.name))]));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ExpressionBuilder updatedAt(ExpressionBuilder dt) {
|
||||
if (autoIdAndDateFields == false) return dt;
|
||||
return dt
|
||||
.notEquals(literal(null))
|
||||
.ternary(dt, lib$core.DateTime.newInstance([], constructor: 'now'));
|
||||
}
|
||||
}
|
|
@ -8,6 +8,6 @@ part 'author.g.dart';
|
|||
@serializable
|
||||
@orm
|
||||
class _Author extends Model {
|
||||
@Column(length: 255, index: IndexType.UNIQUE, defaultValue: 'Tobe Osakwe')
|
||||
@Column(length: 255, indexType: IndexType.UNIQUE, defaultValue: 'Tobe Osakwe')
|
||||
String name;
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ part 'tree.g.dart';
|
|||
@serializable
|
||||
@orm
|
||||
class _Tree extends Model {
|
||||
@Column(index: IndexType.UNIQUE, type: ColumnType.SMALL_INT)
|
||||
@Column(indexType: IndexType.UNIQUE, type: ColumnType.smallInt)
|
||||
int rings;
|
||||
|
||||
@hasMany
|
||||
|
|
|
@ -28,9 +28,9 @@ main() {
|
|||
'Mazda',
|
||||
'CX9',
|
||||
true,
|
||||
DATE_YMD_HMS.format(MILENNIUM),
|
||||
DATE_YMD_HMS.format(MILENNIUM),
|
||||
DATE_YMD_HMS.format(MILENNIUM)
|
||||
dateYmdHms.format(MILENNIUM),
|
||||
dateYmdHms.format(MILENNIUM),
|
||||
dateYmdHms.format(MILENNIUM)
|
||||
];
|
||||
print(row);
|
||||
var car = CarQuery.parseRow(row);
|
||||
|
@ -167,7 +167,7 @@ main() {
|
|||
expect(car.description, 'Hello');
|
||||
expect(car.familyFriendly, isTrue);
|
||||
expect(
|
||||
DATE_YMD_HMS.format(car.recalledAt), DATE_YMD_HMS.format(recalledAt));
|
||||
dateYmdHms.format(car.recalledAt), dateYmdHms.format(recalledAt));
|
||||
expect(car.createdAt, allOf(isNotNull, equals(car.updatedAt)));
|
||||
});
|
||||
|
||||
|
@ -183,8 +183,8 @@ main() {
|
|||
expect(car.make, beetle.make);
|
||||
expect(car.description, beetle.description);
|
||||
expect(car.familyFriendly, beetle.familyFriendly);
|
||||
expect(DATE_YMD_HMS.format(car.recalledAt),
|
||||
DATE_YMD_HMS.format(beetle.recalledAt));
|
||||
expect(dateYmdHms.format(car.recalledAt),
|
||||
dateYmdHms.format(beetle.recalledAt));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue