orm_service tests

This commit is contained in:
Tobe O 2019-04-20 17:30:48 -04:00
parent 55e8a2e83c
commit 4bc50eca69
7 changed files with 726 additions and 7 deletions

View file

@ -1,3 +1,6 @@
# 2.0.1
* Gracefully handle `null` in enum fields.
# 2.0.0+2 # 2.0.0+2
* Widen `analyzer` dependency range. * Widen `analyzer` dependency range.

View file

@ -192,7 +192,9 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
.property('tryParse') .property('tryParse')
.call([expr.property('toString').call([])]); .call([expr.property('toString').call([])]);
} else if (fType is InterfaceType && fType.element.isEnum) { } else if (fType is InterfaceType && fType.element.isEnum) {
expr = type.property('values').index(expr.asA(refer('int'))); var isNull = expr.equalTo(literalNull);
expr = isNull.conditional(literalNull,
type.property('values').index(expr.asA(refer('int'))));
} else } else
expr = expr.asA(type); expr = expr.asA(type);
@ -691,7 +693,7 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
Expression value = refer('value'); Expression value = refer('value');
if (fType is InterfaceType && fType.element.isEnum) { if (fType is InterfaceType && fType.element.isEnum) {
value = value.property('index'); value = CodeExpression(Code('value?.index'));
} else if (const TypeChecker.fromRuntime(List) } else if (const TypeChecker.fromRuntime(List)
.isAssignableFromType(fType)) { .isAssignableFromType(fType)) {
value = refer('json').property('encode').call([value]); value = refer('json').property('encode').call([value]);

21
angel_orm_service/LICENSE Normal file
View file

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

View file

@ -63,7 +63,21 @@ class OrmService<Id, Data, TQuery extends Query<Data, QueryWhere>>
await queryObj(query); await queryObj(query);
} else if (queryObj is Map) { } else if (queryObj is Map) {
queryObj.forEach((k, v) { queryObj.forEach((k, v) {
if (k is String && v is! RequestContext && v is! ResponseContext) { if (k == r'$sort') {
if (v is Map) {
v.forEach((key, value) {
var descending = false;
if (value is String)
descending = value == '-1';
else if (value is num) descending = value.toInt() == -1;
query.orderBy(key.toString(), descending: descending);
});
} else if (v is String) {
query.orderBy(v);
}
} else if (k is String &&
v is! RequestContext &&
v is! ResponseContext) {
_apply(query, k, v); _apply(query, k, v);
} }
}); });
@ -74,6 +88,10 @@ class OrmService<Id, Data, TQuery extends Query<Data, QueryWhere>>
@override @override
Future<List<Data>> readMany(List<Id> ids, Future<List<Data>> readMany(List<Id> ids,
[Map<String, dynamic> params]) async { [Map<String, dynamic> params]) async {
if (ids.isEmpty) {
throw ArgumentError.value(ids, 'ids', 'cannot be empty');
}
var query = await queryCreator(); var query = await queryCreator();
var builder = _findBuilder(query, idField); var builder = _findBuilder(query, idField);
@ -134,7 +152,6 @@ class OrmService<Id, Data, TQuery extends Query<Data, QueryWhere>>
@override @override
Future<Data> modify(Id id, Data data, [Map<String, dynamic> params]) { Future<Data> modify(Id id, Data data, [Map<String, dynamic> params]) {
// TODO: Is there any way to make this an actual modify, and not an update?
return update(id, data, params); return update(id, data, params);
} }
@ -151,7 +168,10 @@ class OrmService<Id, Data, TQuery extends Query<Data, QueryWhere>>
'${query.values.runtimeType} has no `copyFrom` method, but OrmService requires this for updates.'); '${query.values.runtimeType} has no `copyFrom` method, but OrmService requires this for updates.');
} }
return await query.updateOne(executor); var result = await query.updateOne(executor);
if (result != null) return result;
throw new AngelHttpException.notFound(
message: 'No record found for ID $id');
} }
@override @override
@ -170,7 +190,9 @@ class OrmService<Id, Data, TQuery extends Query<Data, QueryWhere>>
await _applyQuery(query, params); await _applyQuery(query, params);
} }
var deleted = await query.delete(executor); var result = await query.deleteOne(executor);
return deleted.isEmpty ? null : deleted.first; if (result != null) return result;
throw new AngelHttpException.notFound(
message: 'No record found for ID $id');
} }
} }

View file

@ -0,0 +1,233 @@
import 'dart:async';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_orm_postgres/angel_orm_postgres.dart';
import 'package:angel_orm_service/angel_orm_service.dart';
import 'package:logging/logging.dart';
import 'package:postgres/postgres.dart';
import 'package:test/test.dart';
import 'pokemon.dart';
void main() {
Logger logger;
PostgreSqlExecutor executor;
Service<int, Pokemon> pokemonService;
setUp(() async {
var conn = PostgreSQLConnection('localhost', 5432, 'angel_orm_service_test',
username: Platform.environment['POSTGRES_USERNAME'] ?? 'postgres',
password: Platform.environment['POSTGRES_PASSWORD'] ?? 'password');
hierarchicalLoggingEnabled = true;
logger = Logger.detached('orm_service');
logger.level = Level.ALL;
if (Platform.environment['log'] == '1') logger.onRecord.listen(print);
executor = PostgreSqlExecutor(conn, logger: logger);
await conn.open();
await conn.query('''
CREATE TEMPORARY TABLE pokemons (
id serial,
species varchar,
name varchar,
level integer,
type1 integer,
type2 integer,
created_at timestamp,
updated_at timestamp
);
''');
pokemonService = OrmService(executor, () => PokemonQuery());
});
tearDown(() async {
await executor.close();
pokemonService.close();
logger.clearListeners();
});
test('create', () async {
var blaziken = await pokemonService.create(Pokemon(
species: 'Blaziken',
level: 100,
type1: PokemonType.fire,
type2: PokemonType.fighting));
print(blaziken);
expect(blaziken.id, isNotNull);
expect(blaziken.species, 'Blaziken');
expect(blaziken.level, 100);
expect(blaziken.type1, PokemonType.fire);
expect(blaziken.type2, PokemonType.fighting);
});
group('after create', () {
Pokemon giratina, pikachu;
setUp(() async {
giratina = await pokemonService.create(Pokemon(
species: 'Giratina',
name: 'My First Legendary',
level: 54,
type1: PokemonType.ghost,
type2: PokemonType.dragon));
pikachu = await pokemonService.create(Pokemon(
species: 'Pikachu',
level: 100,
type1: PokemonType.electric,
));
});
group('index', () {
test('default', () async {
expect(await pokemonService.index(), contains(giratina));
expect(await pokemonService.index(), contains(pikachu));
});
test('with callback', () async {
var result = await pokemonService.index({
'query': (PokemonQuery query) async {
query.where.level.equals(pikachu.level);
},
});
expect(result, [pikachu]);
});
test('search params', () async {
Future<List<Pokemon>> searchByType1(PokemonType type1) async {
var query = {PokemonFields.type1: type1};
var params = {'query': query};
return await pokemonService.index(params);
}
expect(await searchByType1(PokemonType.ghost), [giratina]);
expect(await searchByType1(PokemonType.electric), [pikachu]);
expect(await searchByType1(PokemonType.grass), []);
});
group(r'$sort', () {
test('by name', () async {
expect(
await pokemonService.index({
'query': {r'$sort': 'level'}
}),
[giratina, pikachu]);
});
test('map number', () async {
expect(
await pokemonService.index({
'query': {
r'$sort': {'type1': -1}
}
}),
[giratina, pikachu]);
expect(
await pokemonService.index({
'query': {
r'$sort': {'type1': 100}
}
}),
[pikachu, giratina]);
});
test('map string', () async {
expect(
await pokemonService.index({
'query': {
r'$sort': {'type1': '-1'}
}
}),
[giratina, pikachu]);
expect(
await pokemonService.index({
'query': {
r'$sort': {'type1': 'foo'}
}
}),
[pikachu, giratina]);
});
});
});
group('findOne', () {
test('default', () async {
expect(
await pokemonService.findOne({
'query': {PokemonFields.name: giratina.name}
}),
giratina);
expect(
await pokemonService.findOne({
'query': {PokemonFields.level: pikachu.level}
}),
pikachu);
expect(
() => pokemonService.findOne({
'query': {PokemonFields.level: pikachu.level * 3}
}),
throwsA(TypeMatcher<AngelHttpException>()));
});
test('nonexistent throws 404', () {
expect(
() => pokemonService.findOne({
'query': {PokemonFields.type1: PokemonType.poison}
}),
throwsA(TypeMatcher<AngelHttpException>()));
});
});
group('read', () {
test('default', () async {
expect(await pokemonService.read(pikachu.idAsInt), pikachu);
expect(await pokemonService.read(giratina.idAsInt), giratina);
});
test('nonexistent throws 404', () {
expect(() => pokemonService.read(999),
throwsA(TypeMatcher<AngelHttpException>()));
});
});
test('readMany', () async {
expect(pokemonService.readMany([giratina.idAsInt, pikachu.idAsInt]),
completion([giratina, pikachu]));
expect(
pokemonService.readMany([giratina.idAsInt]), completion([giratina]));
expect(pokemonService.readMany([pikachu.idAsInt]), completion([pikachu]));
expect(() => pokemonService.readMany([]), throwsArgumentError);
});
group('update', () {
test('default', () async {
expect(
await pokemonService.update(
giratina.idAsInt, giratina.copyWith(name: 'Hello')),
giratina.copyWith(name: 'Hello'));
});
test('nonexistent throws 404', () {
expect(
() => pokemonService.update(999, giratina.copyWith(name: 'Hello')),
throwsA(TypeMatcher<AngelHttpException>()));
});
});
group('remove', () {
test('default', () async {
expect(pokemonService.read(giratina.idAsInt), completion(giratina));
expect(pokemonService.read(pikachu.idAsInt), completion(pikachu));
});
test('nonexistent throws 404', () {
expect(() => pokemonService.remove(999),
throwsA(TypeMatcher<AngelHttpException>()));
});
test('cannot remove all unless explicitly set', () async {
expect(() => pokemonService.remove(null, {'provider': Providers.rest}),
throwsA(TypeMatcher<AngelHttpException>()));
});
});
});
}

View file

@ -0,0 +1,33 @@
import 'package:angel_migration/angel_migration.dart';
import 'package:angel_serialize/angel_serialize.dart';
import 'package:angel_orm/angel_orm.dart';
part 'pokemon.g.dart';
enum PokemonType {
fire,
grass,
water,
dragon,
poison,
dark,
fighting,
electric,
ghost
}
@serializable
@orm
abstract class _Pokemon extends Model {
@notNull
String get species;
String get name;
@notNull
int get level;
@notNull
PokemonType get type1;
PokemonType get type2;
}

View file

@ -0,0 +1,405 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'pokemon.dart';
// **************************************************************************
// MigrationGenerator
// **************************************************************************
class PokemonMigration extends Migration {
@override
up(Schema schema) {
schema.create('pokemons', (table) {
table.serial('id')..primaryKey();
table.varChar('species');
table.varChar('name');
table.integer('level');
table.integer('type1');
table.integer('type2');
table.timeStamp('created_at');
table.timeStamp('updated_at');
});
}
@override
down(Schema schema) {
schema.drop('pokemons');
}
}
// **************************************************************************
// OrmGenerator
// **************************************************************************
class PokemonQuery extends Query<Pokemon, PokemonQueryWhere> {
PokemonQuery({Set<String> trampoline}) {
trampoline ??= Set();
trampoline.add(tableName);
_where = PokemonQueryWhere(this);
}
@override
final PokemonQueryValues values = PokemonQueryValues();
PokemonQueryWhere _where;
@override
get casts {
return {};
}
@override
get tableName {
return 'pokemons';
}
@override
get fields {
return const [
'id',
'species',
'name',
'level',
'type1',
'type2',
'created_at',
'updated_at'
];
}
@override
PokemonQueryWhere get where {
return _where;
}
@override
PokemonQueryWhere newWhereClause() {
return PokemonQueryWhere(this);
}
static Pokemon parseRow(List row) {
if (row.every((x) => x == null)) return null;
var model = Pokemon(
id: row[0].toString(),
species: (row[1] as String),
name: (row[2] as String),
level: (row[3] as int),
type1: row[4] == null ? null : PokemonType.values[(row[4] as int)],
type2: row[5] == null ? null : PokemonType.values[(row[5] as int)],
createdAt: (row[6] as DateTime),
updatedAt: (row[7] as DateTime));
return model;
}
@override
deserialize(List row) {
return parseRow(row);
}
}
class PokemonQueryWhere extends QueryWhere {
PokemonQueryWhere(PokemonQuery query)
: id = NumericSqlExpressionBuilder<int>(query, 'id'),
species = StringSqlExpressionBuilder(query, 'species'),
name = StringSqlExpressionBuilder(query, 'name'),
level = NumericSqlExpressionBuilder<int>(query, 'level'),
type1 = EnumSqlExpressionBuilder<PokemonType>(
query, 'type1', (v) => v.index),
type2 = EnumSqlExpressionBuilder<PokemonType>(
query, 'type2', (v) => v.index),
createdAt = DateTimeSqlExpressionBuilder(query, 'created_at'),
updatedAt = DateTimeSqlExpressionBuilder(query, 'updated_at');
final NumericSqlExpressionBuilder<int> id;
final StringSqlExpressionBuilder species;
final StringSqlExpressionBuilder name;
final NumericSqlExpressionBuilder<int> level;
final EnumSqlExpressionBuilder<PokemonType> type1;
final EnumSqlExpressionBuilder<PokemonType> type2;
final DateTimeSqlExpressionBuilder createdAt;
final DateTimeSqlExpressionBuilder updatedAt;
@override
get expressionBuilders {
return [id, species, name, level, type1, type2, createdAt, updatedAt];
}
}
class PokemonQueryValues extends MapQueryValues {
@override
get casts {
return {};
}
String get id {
return (values['id'] as String);
}
set id(String value) => values['id'] = value;
String get species {
return (values['species'] as String);
}
set species(String value) => values['species'] = value;
String get name {
return (values['name'] as String);
}
set name(String value) => values['name'] = value;
int get level {
return (values['level'] as int);
}
set level(int value) => values['level'] = value;
PokemonType get type1 {
return PokemonType.values[(values['type1'] as int)];
}
set type1(PokemonType value) => values['type1'] = value?.index;
PokemonType get type2 {
return PokemonType.values[(values['type2'] as int)];
}
set type2(PokemonType value) => values['type2'] = value?.index;
DateTime get createdAt {
return (values['created_at'] as DateTime);
}
set createdAt(DateTime value) => values['created_at'] = value;
DateTime get updatedAt {
return (values['updated_at'] as DateTime);
}
set updatedAt(DateTime value) => values['updated_at'] = value;
void copyFrom(Pokemon model) {
species = model.species;
name = model.name;
level = model.level;
type1 = model.type1;
type2 = model.type2;
createdAt = model.createdAt;
updatedAt = model.updatedAt;
}
}
// **************************************************************************
// JsonModelGenerator
// **************************************************************************
@generatedSerializable
class Pokemon extends _Pokemon {
Pokemon(
{this.id,
@required this.species,
this.name,
@required this.level,
@required this.type1,
this.type2,
this.createdAt,
this.updatedAt});
@override
final String id;
@override
final String species;
@override
final String name;
@override
final int level;
@override
final PokemonType type1;
@override
final PokemonType type2;
@override
final DateTime createdAt;
@override
final DateTime updatedAt;
Pokemon copyWith(
{String id,
String species,
String name,
int level,
PokemonType type1,
PokemonType type2,
DateTime createdAt,
DateTime updatedAt}) {
return new Pokemon(
id: id ?? this.id,
species: species ?? this.species,
name: name ?? this.name,
level: level ?? this.level,
type1: type1 ?? this.type1,
type2: type2 ?? this.type2,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt);
}
bool operator ==(other) {
return other is _Pokemon &&
other.id == id &&
other.species == species &&
other.name == name &&
other.level == level &&
other.type1 == type1 &&
other.type2 == type2 &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt;
}
@override
int get hashCode {
return hashObjects(
[id, species, name, level, type1, type2, createdAt, updatedAt]);
}
@override
String toString() {
return "Pokemon(id=$id, species=$species, name=$name, level=$level, type1=$type1, type2=$type2, createdAt=$createdAt, updatedAt=$updatedAt)";
}
Map<String, dynamic> toJson() {
return PokemonSerializer.toMap(this);
}
}
// **************************************************************************
// SerializerGenerator
// **************************************************************************
const PokemonSerializer pokemonSerializer = const PokemonSerializer();
class PokemonEncoder extends Converter<Pokemon, Map> {
const PokemonEncoder();
@override
Map convert(Pokemon model) => PokemonSerializer.toMap(model);
}
class PokemonDecoder extends Converter<Map, Pokemon> {
const PokemonDecoder();
@override
Pokemon convert(Map map) => PokemonSerializer.fromMap(map);
}
class PokemonSerializer extends Codec<Pokemon, Map> {
const PokemonSerializer();
@override
get encoder => const PokemonEncoder();
@override
get decoder => const PokemonDecoder();
static Pokemon fromMap(Map map) {
if (map['species'] == null) {
throw new FormatException("Missing required field 'species' on Pokemon.");
}
if (map['level'] == null) {
throw new FormatException("Missing required field 'level' on Pokemon.");
}
if (map['type1'] == null) {
throw new FormatException("Missing required field 'type1' on Pokemon.");
}
return new Pokemon(
id: map['id'] as String,
species: map['species'] as String,
name: map['name'] as String,
level: map['level'] as int,
type1: map['type1'] is PokemonType
? (map['type1'] as PokemonType)
: (map['type1'] is int
? PokemonType.values[map['type1'] as int]
: null),
type2: map['type2'] is PokemonType
? (map['type2'] as PokemonType)
: (map['type2'] is int
? PokemonType.values[map['type2'] as int]
: null),
createdAt: map['created_at'] != null
? (map['created_at'] is DateTime
? (map['created_at'] as DateTime)
: DateTime.parse(map['created_at'].toString()))
: null,
updatedAt: map['updated_at'] != null
? (map['updated_at'] is DateTime
? (map['updated_at'] as DateTime)
: DateTime.parse(map['updated_at'].toString()))
: null);
}
static Map<String, dynamic> toMap(_Pokemon model) {
if (model == null) {
return null;
}
if (model.species == null) {
throw new FormatException("Missing required field 'species' on Pokemon.");
}
if (model.level == null) {
throw new FormatException("Missing required field 'level' on Pokemon.");
}
if (model.type1 == null) {
throw new FormatException("Missing required field 'type1' on Pokemon.");
}
return {
'id': model.id,
'species': model.species,
'name': model.name,
'level': model.level,
'type1':
model.type1 == null ? null : PokemonType.values.indexOf(model.type1),
'type2':
model.type2 == null ? null : PokemonType.values.indexOf(model.type2),
'created_at': model.createdAt?.toIso8601String(),
'updated_at': model.updatedAt?.toIso8601String()
};
}
}
abstract class PokemonFields {
static const List<String> allFields = <String>[
id,
species,
name,
level,
type1,
type2,
createdAt,
updatedAt
];
static const String id = 'id';
static const String species = 'species';
static const String name = 'name';
static const String level = 'level';
static const String type1 = 'type1';
static const String type2 = 'type2';
static const String createdAt = 'created_at';
static const String updatedAt = 'updated_at';
}