diff --git a/README.md b/README.md index 6fa4241..09cd405 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ You can annotate a `@Model` as per the following simple example ```dart import 'package:shellstone/shellstone.dart'; -// Annotate this class as being a Model class with identity user +// Annotate this class as being a Model class which corresponds table 'user' @Model(name: 'user', source: 'mysql') class User { diff --git a/example/planning.dart b/example/planning.dart index ad38be3..7868afe 100644 --- a/example/planning.dart +++ b/example/planning.dart @@ -1,42 +1,28 @@ import '../lib/shellstone.dart'; -// Models can be defined with annotations -@Model(name: 'user') +@Model(migration: 'drop') // 'name' is auto set to Classname in lowercase e.g 'user' class User { - // Attributes are specified in the same way - @Attr(type: 'integer', primaryKey: true) int id; - @Attr(type: 'string') int username; - @Attr(type: 'string') int password; - @Attr(type: 'boolean') bool archived; + // Attribute definitions + @Attr(primaryKey: true) int id; + @Attr() String username; + @Attr() String password; - // Relations to come (this annotation is conceptual) - // @Rel('1:n') List roles; + // Has one to many 'roles', the model relation is inferred by + @Rel(by: 'id', as: 'user_id') List roles; } -// @Model('person') -// class Person extends BaseModel with Transactional { -// //<== CONCEPTS -// // @Attr(column: '_id') int id; Provided by BaseModel -// // @Attr() DateTime createdAt; ^^ -// // @Attr() DateTime updatedAt; ^^ -// -// // save(); Provided by Transactional -// // rollback(); ^^ -// -// @Attr(type: 'string') String firstName; -// @Attr(type: 'string') String lastName; -// } +@Model(migration: 'drop') +class Role { + @Attr(primaryKey: true) int id; -// main() async { -// // Setup Shellstone -// await Shellstone.setup(); -// -// // Get the first user where it matches the query -// User user = await Model.find('User').where('username').eq('1234').run(); -// -// // Get user using filter -// user = await Model.find('User').filter((user) => user.lastName == 'Smith').run(); -// -// // Find all users -// Stream users = await Model.findAll('User').run(); -// } + // Belongs to user, using model type for example if type cant be inferred + @Rel() User user; +} + +main() async { + // Setup Shellstone + await strapIn(); + + // // Get the first user where it matches the query + // User user = await Model.find('User').where('username').eq('1234').incl('roles').run(); +} diff --git a/example/scratchpad.dart b/example/scratchpad.dart index c0d9f68..30589d5 100644 --- a/example/scratchpad.dart +++ b/example/scratchpad.dart @@ -1,15 +1,16 @@ import 'dart:async'; import 'package:shellstone/shellstone.dart'; -main() async { - await strapIn(); +@Model(migration: 'drop') +class User { + @Attr(primaryKey:true) int id; + @Attr() String firstName; + @Attr() String lastName; + @Attr() String username; + @Attr() String password; } +main() async { + await strapIn(); -// Javascript Examples -// User.find({ where: { name: 'foo' }, skip: 20, limit: 10, sort: 'name DESC' }); -// User.find({ name: { '!' : ['Walter', 'Skyler'] }}); - -// [Expected] Shellstone Examples -// Model.find('User').where(['name']).eq('foo').skip(20).limit(10).sort(['name'],Desc).run(); -// Model.find('User').where(['name']).ne(['Walter','Skyler']).run(); +} diff --git a/lib/shellstone.dart b/lib/shellstone.dart index 8413d38..151ebc3 100644 --- a/lib/shellstone.dart +++ b/lib/shellstone.dart @@ -21,6 +21,8 @@ export 'src/metadata/metadata_proxies.dart'; export 'src/datalayer/database_adapter.dart'; export 'src/datalayer/querylang.dart'; export 'src/datalayer/schema/schema.dart'; +export 'src/datalayer/schema/schema_field.dart'; +export 'src/datalayer/schema/schema_relation.dart'; export 'src/events/events.dart'; export 'src/events/event_registration.dart'; export 'src/entities/entity_wrapper.dart'; @@ -75,16 +77,26 @@ addAdapter(String name, DatabaseAdapter adapter) { /// /// **Note** you cant listen to events that trigger during setup such as some /// adapter events for example. To listen on those you must use the @Listen annotation -addListener(EventRegistration reg, Function f, [loc = 'pre']) { - addHandler(Listen, reg, f, loc); +addListener(EventRegistration reg, Function f) { + addHandler(Listen, reg, f); } /// Adds a hook for a given [EventRegistration] /// /// **Note** you cant listen to events that trigger during setup such as some /// adapter events for example. To listen on those you must use the @Listen annotation -addHook(EventRegistration reg, Function f, [loc = 'pre']) { - addHandler(Listen, reg, f, loc); +addHook(EventRegistration reg, Function f) { + addHandler(Hook, reg, f); +} + +/// Removes a hook for a given [EventRegistration] +removeListener(EventRegistration reg, Function f) { + removeHandler(Listen, reg, f); +} + +/// Removes a hook for a given [EventRegistration] +removeHook(EventRegistration reg, Function f) { + removeHandler(Hook, reg, f); } /// Allows for the triggering of some [Event] e @@ -145,4 +157,8 @@ _loadSchemas() { // Construct the schema which will slam it into the cache meta.forEach((name, proxy) => new Schema.fromMetadata(name, proxy)); + + // Copies relation keys into their schemas, at least it only happens once + // otherwise a nicer solution might be better for this... + Schema.getAll().forEach((schema) => schema.transferDerivedFields()); } diff --git a/lib/src/datalayer/adapters/postgres/postgres_query_executor.dart b/lib/src/datalayer/adapters/postgres/postgres_query_executor.dart index f47b05b..904965f 100644 --- a/lib/src/datalayer/adapters/postgres/postgres_query_executor.dart +++ b/lib/src/datalayer/adapters/postgres/postgres_query_executor.dart @@ -19,7 +19,6 @@ class PostgresQueryExecutor extends SqlExecutor { // Execute some sql executeSql(sql, [bool release]) async { - // conn = await psql.connect(adapter.uri); conn = await adapter.pool.connect(); var result; diff --git a/lib/src/datalayer/adapters/sql_builder.dart b/lib/src/datalayer/adapters/sql_builder.dart index c971686..8e8f1a3 100644 --- a/lib/src/datalayer/adapters/sql_builder.dart +++ b/lib/src/datalayer/adapters/sql_builder.dart @@ -17,6 +17,7 @@ abstract class SqlBuilder { // Add the table creation statements return _schemas.fold(new List(), (list, schema) { list.addAll(getTableStatements(schema)); + if (schema.indexes.isNotEmpty) list.addAll(getIndexStatements(schema)); return list; }); } @@ -34,7 +35,7 @@ abstract class SqlBuilder { buffer.write('create table if not exists ${schema.resource}'); // Buffer the field lines - var fields = schema.fields.values.fold(new List(), (list,field) { + var fields = schema.allFields.values.fold(new List(), (list,field) { list.add(getFieldLine(field)); return list; }).join(','); @@ -77,5 +78,13 @@ abstract class SqlBuilder { return result.join(' '); } + // Get the statements to add all the indexes + List getIndexStatements(Schema schema) { + return schema.indexes.values.fold(new List(),(list,field) { + list.add('alter table ${schema.resource} add index(${field.column})'); + return list; + }); + } + String getPrimaryKey(Schema schema) => 'primary key (${schema.primaryKey.column})'; } diff --git a/lib/src/datalayer/querylang.dart b/lib/src/datalayer/querylang.dart index 25a37fb..9fa03a3 100644 --- a/lib/src/datalayer/querylang.dart +++ b/lib/src/datalayer/querylang.dart @@ -8,6 +8,7 @@ export 'querylang/tokens/modifier.dart'; export 'querylang/tokens/selector.dart'; export 'querylang/tokens/condition.dart'; export 'querylang/tokens/indentifier.dart'; +export 'querylang/tokens/inclusion.dart'; export 'querylang/tokens/insertion.dart'; export 'querylang/tokens/update.dart'; export 'querylang/tokens/removal.dart'; diff --git a/lib/src/datalayer/querylang/query_action.dart b/lib/src/datalayer/querylang/query_action.dart index a48789e..5c4e319 100644 --- a/lib/src/datalayer/querylang/query_action.dart +++ b/lib/src/datalayer/querylang/query_action.dart @@ -19,10 +19,11 @@ class QueryAction { QueryChain _chain; // Constructor - QueryAction(String name) { + QueryAction(String name,[this._chain]) { this.name = name; - _chain = new QueryChain()..setQueryAction(this); model = Metadata.model(name); + if (_chain == null) _chain = new QueryChain()..setQueryAction(this); + else _chain.setQueryAction(this); } dynamic _init(type, result) { diff --git a/lib/src/datalayer/querylang/query_runner.dart b/lib/src/datalayer/querylang/query_runner.dart index 6b0f360..2e5daa7 100644 --- a/lib/src/datalayer/querylang/query_runner.dart +++ b/lib/src/datalayer/querylang/query_runner.dart @@ -4,6 +4,7 @@ import '../../../shellstone.dart'; // Ugh hmmm class QueryRunner implements SingleResultRunnable, MultipleResultRunnable { QueryChain _chain; + List _includes = []; QueryRunner(this._chain); @@ -16,4 +17,12 @@ class QueryRunner implements SingleResultRunnable, MultipleResultRunnable { // Return the result of the query adapter run return adapter.execute(_chain); } + + // Handles an include call for the governed chain + handleInclude(entity, chain) { + // The provided chain must match the parent! + if (chain != _chain) return; + + + } } diff --git a/lib/src/datalayer/querylang/query_token.dart b/lib/src/datalayer/querylang/query_token.dart index ecfc394..a92aca3 100644 --- a/lib/src/datalayer/querylang/query_token.dart +++ b/lib/src/datalayer/querylang/query_token.dart @@ -13,11 +13,12 @@ import 'query_runner.dart'; /// Essentially the classes here are all part of a Query abstract class QueryToken { QueryChain _chain; + QueryRunner runner; String operator; Iterable args; /// Takes a single [QueryChain] as an argument - QueryToken(this._chain); + QueryToken(this._chain,[QueryRunner this.runner]); // Allows conveniently setting the operator, values and returning the result dynamic init(op, val, result) { @@ -32,7 +33,7 @@ abstract class QueryToken { return result; } - runChain() => new QueryRunner(chain).run(); + runChain() => runner == null ? new QueryRunner(chain).run() : runner.run(); get chain => _chain; } diff --git a/lib/src/datalayer/querylang/tokens/inclusion.dart b/lib/src/datalayer/querylang/tokens/inclusion.dart new file mode 100644 index 0000000..3a53be0 --- /dev/null +++ b/lib/src/datalayer/querylang/tokens/inclusion.dart @@ -0,0 +1,16 @@ +import 'query.dart'; +import '../query_chain.dart'; +import '../query_token.dart'; +import '../query_runner.dart'; +import '../query_action.dart'; + +/// Defines a class which specifies an included relation on the chain +class Inclusion extends QueryToken { + Inclusion(QueryChain chain) : super(chain); + + /// Takes an [id] and returns the [QueryChain] + MultipleResultQuery incl(String name) { + // + // var action = new QueryAction('User'); + } +} diff --git a/lib/src/datalayer/schema/schema.dart b/lib/src/datalayer/schema/schema.dart index b570dd2..8e7a5fe 100644 --- a/lib/src/datalayer/schema/schema.dart +++ b/lib/src/datalayer/schema/schema.dart @@ -1,5 +1,7 @@ import 'schema_field.dart'; +import 'schema_relation.dart'; import '../../metadata/metadata_proxies.dart'; +import '../../metadata/annotations.dart'; import '../../internal/globals.dart'; /// Acts as an interpretation of a form of `schema` in shellstone terms @@ -10,18 +12,21 @@ import '../../internal/globals.dart'; /// possible. class Schema { // Cache of all schemas - static Map _schemas = {}; - Map _fields = {}; // All fields - Map _indexes = {}; // Fields that are index: true - Map _columns = {}; // Fields by their column name + static Map _schemas = {}; + Map _fields = {}; // All fields + Map _indexes = {}; // Fields that are index: true + Map _columns = {}; // Fields by their column name + Map _derived = {}; // Fields that ref another Schema + Map _relations = {}; ModelMetadata _meta; String name; SchemaField primaryKey; - Schema._(this.name,[this._meta]) { - // Build the schema fields out + Schema._(this.name, [this._meta]) { + // Build the schema fields and relations _buildFields(); + _buildRels(); } /// Gets a schema by [name] @@ -35,17 +40,17 @@ class Schema { static Iterable getAll() => _schemas.values; /// Creates a new schema or retrieves from the cache - factory Schema.fromMetadata(name,ModelMetadata meta){ + factory Schema.fromMetadata(name, ModelMetadata meta) { // Schema is in the cache so return it if (_schemas.containsKey(name)) return _schemas[name]; // Else create and return new - return _schemas.putIfAbsent(name, () => new Schema._(name,meta)); + return _schemas.putIfAbsent(name, () => new Schema._(name, meta)); } // Loads up the fields for the schema into the various flattened collections _buildFields() { - _meta.dependents.forEach((name,attr) { + _meta.attributes.forEach((name, attr) { var field = new SchemaField(this, name, attr); // Add to fields and columns @@ -58,6 +63,40 @@ class Schema { }); } + // Builds the relations / associations between models + _buildRels() { + _meta.relations.forEach((name, rel) { + var relation = new SchemaRelation(this, name, rel); + + // Add to relations + _relations[name] = relation; + }); + } + + // Updates schemas with new derived fields from their relations + transferDerivedFields() { + _relations.forEach((name, rel) { + // Get the schema which this relation is pointing to + var schema = Schema.get(rel.model.toString()); + + // Get relevant schema field from this schema + var name = rel.as; + var field = getField(rel.by); + var derived = new SchemaField.copy(field, + name: name, + attr: new Attr( + type: field.type, + column: name, + index: true, + length: field.length), + derived: true); + + // Add to derived fields and indexes + schema._derived[name] = derived; + schema._indexes[name] = derived; + }); + } + /// Gets the resource name, which is usually a table or collection String get resource => _meta.model.name; @@ -68,14 +107,30 @@ class Schema { String get migration => _meta.model.migration; /// Retrieves all fields as written per the object - Map get fields => _fields; + Map get fields => _fields; + + /// Returns all fields including derived, useful for table creation + Map get allFields { + var all = new Map.from(_fields); + all.addAll(_derived); + return all; + } /// Retrieves all indexes as written per the object - Map get indexes => _indexes; + Map get indexes => _indexes; + + /// Retrieves all fields which are attached through relations + Map get derived => _derived; /// Retrieves a single field by name SchemaField getField(String name) => fields[name]; /// Retrieves a single field by its column, which _may_ be the same as the name SchemaField getColumn(String name) => _columns[name]; + + /// Retreives a single derived field by name (which is the column name) + SchemaField getDerived(String name) => _derived[name]; + + /// Retrieves a schema relation by its name + SchemaRelation getRelation(String name) => _relations[name]; } diff --git a/lib/src/datalayer/schema/schema_field.dart b/lib/src/datalayer/schema/schema_field.dart index 15fb6e7..683e82d 100644 --- a/lib/src/datalayer/schema/schema_field.dart +++ b/lib/src/datalayer/schema/schema_field.dart @@ -13,11 +13,20 @@ class SchemaField { String name; Attr _attr; EntityDefinition _def; + bool derived; // Indicates the field is derived from a relation - SchemaField(this._schema, this.name, Attr this._attr) { + SchemaField(this._schema, this.name, Attr this._attr, + [this.derived = false]) { _def = EntityBuilder.getDefinition(_schema.name); } + // Field copy, Apparently dartfmt makes this pretty ugly + factory SchemaField.copy(SchemaField field, + {schema, name, Attr attr, derived}) { + return new SchemaField(schema ?? field._schema, name ?? field.name, + attr ?? field._attr, derived ?? field.derived); + } + // Getters get type => _attr.type ?? _convertType(_def.fieldType(name)); get index => _attr.index; diff --git a/lib/src/datalayer/schema/schema_relation.dart b/lib/src/datalayer/schema/schema_relation.dart index fd6a67e..df268f4 100644 --- a/lib/src/datalayer/schema/schema_relation.dart +++ b/lib/src/datalayer/schema/schema_relation.dart @@ -1,2 +1,31 @@ -// Coming soon.... -class SchemaRelation { } +import 'schema.dart'; +// import '../../metadata/annotations.dart'; +import '../../metadata/metadata_proxies.dart'; +import '../../entities/entity_builder.dart'; +import '../../entities/entity_definition.dart'; + + +/// Represents a relation in the schema, +class SchemaRelation { + Schema _schema; + String name; + RelWrapper _rel; + EntityDefinition _def; + + SchemaRelation(this._schema, this.name, this._rel) { + _def = EntityBuilder.getDefinition(_schema.name); + } + + // Getters + get model => _rel.model; + get by => _rel.by ?? _schema.primaryKey.name; + get as => _rel.as ?? '${_schema.resource}_${_schema.primaryKey.column}'; + get isCollection => _rel.isCollection; + get modelName => _rel.model.toString(); + + // TODO: Need some kind of enum for relations to indicate easily oneToMany etc. + // get type + + // TODO: This is relevant only for a many to many + // get via => +} diff --git a/lib/src/events/adapter_events.dart b/lib/src/events/adapter_events.dart index ed96dae..2830620 100644 --- a/lib/src/events/adapter_events.dart +++ b/lib/src/events/adapter_events.dart @@ -3,7 +3,5 @@ import '../metadata/annotations.dart'; /// Adapter event to make it more pleasant than calling the Event constructor class AdapterEvent extends Event { - AdapterEvent(name,data,[isPostEvent=false]) : super(Adapter,name,data) { - if (isPostEvent) this.loc = 'post'; - } + AdapterEvent(name,data) : super(Adapter,name,data); } diff --git a/lib/src/events/event_dispatcher.dart b/lib/src/events/event_dispatcher.dart index f6f624a..c78c50d 100644 --- a/lib/src/events/event_dispatcher.dart +++ b/lib/src/events/event_dispatcher.dart @@ -10,13 +10,13 @@ class EventDispatcher { /// Triggers an event by [Type], [name] with the given data. /// /// for example: `triggerEvent(Adapter,'configure',_adapter)` - static Future triggerEvent(Type t, String name, {dynamic data, loc: 'pre'}) => - trigger(new Event(t, name, data, loc)); + static Future triggerEvent(Type t, String name, {dynamic data}) => + trigger(new Event(t, name, data)); /// Triggers an event by a given registration static Future triggerRegistration(EventRegistration reg, - {dynamic data, loc: 'pre'}) => - trigger(new Event(reg.type, reg.name, data, loc)); + {dynamic data}) => + trigger(new Event(reg.type, reg.name, data)); /// Triggers a given [Event] object static Future trigger(Event e) { @@ -37,7 +37,7 @@ class EventDispatcher { StreamController ctrl = new StreamController.broadcast(); Stream stream = ctrl.stream; - listeners[reg].where((EventHandler h) => h.loc == e.loc).forEach((f){ + listeners[reg].forEach((f){ // Add each handler's delegeate as a listener stream.listen(f.delegate); }); @@ -55,7 +55,7 @@ class EventDispatcher { // Create stream from the event Stream stream = new Stream.fromIterable([e]); - hooks[reg].where((EventHandler h) => h.loc == e.loc).forEach((f){ + hooks[reg].forEach((f){ // Add each handler's delegeate into the stream mapping stream = stream.asyncMap((event) => f.delegate(event) ?? event); }); diff --git a/lib/src/events/events.dart b/lib/src/events/events.dart index c12062f..e9b2de6 100644 --- a/lib/src/events/events.dart +++ b/lib/src/events/events.dart @@ -8,19 +8,17 @@ typedef dynamic HandlerFunction(Event e); /// An [EventHandler] is constructed by the [Hook] and [Listen] annotations /// to indicate when and what will occur when the registered [Event] is triggered class EventHandler { - final String loc; final EventRegistration reg; final Function delegate; // Maybe it should be possible to use class methods? - const EventHandler(this.loc,this.reg, this.delegate); + const EventHandler(this.reg, this.delegate); } /// Defines an event class class Event { Type t; String name; - String loc; dynamic data; - Event(this.t,this.name,[this.data,this.loc='pre']); + Event(this.t,this.name,[this.data]); } diff --git a/lib/src/internal/globals.dart b/lib/src/internal/globals.dart index 290e005..9cb2b8e 100644 --- a/lib/src/internal/globals.dart +++ b/lib/src/internal/globals.dart @@ -18,7 +18,7 @@ dynamic invokeMethod(object, name) { } // Add a handler which is a listener or a hook -addHandler(Type t,reg,f,loc) { +addHandler(Type t,reg,f) { List handlers; if (t == Listen) { @@ -30,8 +30,21 @@ addHandler(Type t,reg,f,loc) { // Try to find an entry with the given function handlers.firstWhere((EventHandler h) => h.delegate == f, orElse: () { // Else add it in - handlers.add(new EventHandler(loc, reg, f)); + handlers.add(new EventHandler(reg, f)); }); } +// Remove a handler +removeHandler(Type t,reg,f) { + if (t == Listen) { + if (!listeners.containsKey(reg)) return; + + listeners[reg].removeWhere((EventHandler h) => h.delegate == f); + } else { + if (!hooks.containsKey(reg)) return; + + hooks[reg].removeWhere((EventHandler h) => h.delegate == f); + } +} + final String defaultSource = 'mysql'; diff --git a/lib/src/metadata/annotations.dart b/lib/src/metadata/annotations.dart index 5e5e346..7394d12 100644 --- a/lib/src/metadata/annotations.dart +++ b/lib/src/metadata/annotations.dart @@ -22,7 +22,11 @@ class Model { final bool schema = true; /// Constructs a [Model] const with the given values - const Model({this.name, this.source, this.migration:'safe'}); + const Model({this.name, this.source, this.migration: 'safe'}); + + /// Copy contructor to allow name change + factory Model.copy(String name, Model model) => + new Model(name: name, source: model.source, migration: model.migration); /// Takes a Model [name] e.g. 'User' and returns an [Identifier]. /// @@ -34,7 +38,8 @@ class Model { static SingleResultQuery find(name) => new QueryAction(_convert(name)).find(); /// The [findAll] method is used to find a *all* matching entites - static MultipleResultQuery findAll(name) => new QueryAction(_convert(name)).findAll(); + static MultipleResultQuery findAll(name) => + new QueryAction(_convert(name)).findAll(); /// The [insert] method is used to insert a given set of values static SingleResultRunnable insert(name, values) => @@ -45,7 +50,7 @@ class Model { new QueryAction(Metadata.name(entities)).insertFrom(entities); /// The [update] method is used to update an entity where matches - static SingleResultQuery update(name,values) => + static SingleResultQuery update(name, values) => new QueryAction(_convert(name)).update(values); /// The [updateFrom] method updates from the given entity or entities @@ -61,7 +66,7 @@ class Model { new QueryAction(Metadata.name(entities)).removeFrom(entities); // Shortcut to convert a type to string if required - static String _convert(name) => (name is Type) ? name.toString():name; + static String _convert(name) => (name is Type) ? name.toString() : name; } /// An annotation to represent the metadata of an Attribute. @@ -122,10 +127,33 @@ class Adapter { /// Defines a [Handler] such as a [Hook] or a [Listen]er abstract class Handler { - final String loc; final EventRegistration reg; - const Handler(this.reg, [this.loc = 'pre']); + const Handler(this.reg); +} + +/// An annotation to define a relationship between models +class Rel { + /// The model name that this relationship associates with + final Type model; + + /// The field in THIS model which is referenced by the relation + final String by; + + /// An optional name replacement for the [by] field + final String as; + + /// An optional replacement name for the joining set or table + final String via; + + const Rel({this.model, this.by, this.as, this.via}); + + // Copy constructor + factory Rel.copy(Rel r, {model, by, as, via}) => new Rel( + model: model ?? r.model, + by: by ?? r.by, + as: as ?? r.as, + via: via ?? r.via); } /// An annotation to set a listener for a particular event @@ -134,8 +162,6 @@ abstract class Handler { /// and are generally for notification as they are fired off async class Listen extends Handler { const Listen(EventRegistration reg) : super(reg); - const Listen.pre(EventRegistration reg) : super(reg); - const Listen.post(EventRegistration reg) : super(reg, 'post'); } /// An annotation to set a hook in for a particular event @@ -145,6 +171,4 @@ class Listen extends Handler { /// similar to a map function. class Hook extends Handler { const Hook(EventRegistration reg) : super(reg); - const Hook.pre(EventRegistration reg) : super(reg); - const Hook.post(EventRegistration reg) : super(reg, 'post'); } diff --git a/lib/src/metadata/metadata.dart b/lib/src/metadata/metadata.dart index 5362343..1ad90c9 100644 --- a/lib/src/metadata/metadata.dart +++ b/lib/src/metadata/metadata.dart @@ -26,7 +26,10 @@ class Metadata { static Model model(String name) => _modelProxy(name).model; /// Returns the attributes Map for the model [name] - static Map attr(String name) => _modelProxy(name).dependents; + static Map attr(String name) => _modelProxy(name).attributes; + + /// Returns the relations Map for a given model [name] + static Map rel(String name) => _modelProxy(name).relations; /// Returns the name for the entity or list of entities static String name(dynamic entity) { @@ -42,10 +45,6 @@ class Metadata { /// Returns the [Adapter] metadata by [name] static Adapter adapter(String name) => _adapterProxy(name).adapter; - // /// Returns the [DBAdapter] @event handlers for a given adapter [name] - // static Map handlers(String name) => - // _adapterProxy(name).dependents; - /// Tests the existence of some kind of metadata static bool exists(String type, String name) { return type == 'model' diff --git a/lib/src/metadata/metadata_proxies.dart b/lib/src/metadata/metadata_proxies.dart index 2367a56..a13aa83 100644 --- a/lib/src/metadata/metadata_proxies.dart +++ b/lib/src/metadata/metadata_proxies.dart @@ -3,13 +3,14 @@ import 'annotations.dart'; abstract class MetadataProxy { ClassMirror ref; - Map dependents = new Map(); MetadataProxy(this.ref); } /// Wraps a model by combining the [Model], reflectee and [Attr]ibutes class ModelMetadata extends MetadataProxy { Model model; + Map attributes = new Map(); + Map relations = new Map(); ModelMetadata(ref, this.model) : super(ref); } @@ -21,3 +22,15 @@ class AdapterMetadata extends MetadataProxy { instance = ref.newInstance(const Symbol(''), new List()); } } + +// A hack in to wrap the relations +class RelWrapper implements Rel { + Rel _rel; + bool isCollection; + RelWrapper(this._rel,[this.isCollection=false]); + + get model => _rel.model; + get as => _rel.as; + get by => _rel.by; + get via => _rel.via; +} diff --git a/lib/src/metadata/metadata_scanner.dart b/lib/src/metadata/metadata_scanner.dart index f9b9f02..018e3db 100644 --- a/lib/src/metadata/metadata_scanner.dart +++ b/lib/src/metadata/metadata_scanner.dart @@ -1,6 +1,6 @@ import 'dart:mirrors'; +import 'metadata_proxies.dart'; import '../internal/globals.dart'; -import '../datalayer/schema/schema.dart'; import '../entities/entity_builder.dart'; import '../../shellstone.dart'; @@ -46,11 +46,14 @@ class MetadataScanner { // shellstone adapters (from shellstone.dart) addAdapter( reflectee.name, this.adapters[reflectee.name].instance.reflectee); - } - // Handle Attr annotations - else if (reflectee is Attr) - proxy.dependents[name] = reflectee; + // Handle Attr annotations + } else if (reflectee is Attr) + proxy.attributes[name] = reflectee; + + // Handle relations + else if (reflectee is Rel) + _addRelation(name, m, reflectee, proxy); // Handle Listen or Hook types else if (reflectee is Handler) { @@ -61,7 +64,7 @@ class MetadataScanner { throw 'Invalid handler `$name` provided for `${reflectee.runtimeType}`'; // Set the handlers - addHandler(reflectee.runtimeType, reflectee.reg, fn, reflectee.loc); + addHandler(reflectee.runtimeType, reflectee.reg, fn); } } @@ -71,8 +74,8 @@ class MetadataScanner { MetadataProxy proxy; if (r.runtimeType == Model) { - // If the model has no name give it the class name - r.name ??= name; + // If the model has no name give it the class name via copy + if (r.name == null) r = new Model.copy(name.toLowerCase(), r); map = models; proxy = new ModelMetadata(m, r); @@ -98,6 +101,34 @@ class MetadataScanner { return map; } + // Adds a relation to the model proxy + _addRelation(name, VariableMirror m, Rel r, proxy) { + var model = r.model ?? _deriveType(m); + r = new Rel.copy(r, model: model); + + proxy.relations[name] = new RelWrapper(r,_isCollection(m.type)); + } + + // Determines the type of some variable if possible + _deriveType(VariableMirror m) { + Type t = m.type.reflectedType; + + // Substitute the type if its there is a generic arg + if (m.type.typeArguments.isNotEmpty) { + t = m.type.typeArguments.first.reflectedType; + } + + // This is the one scenario we know will not be valid on scan + if (t == dynamic) + throw 'Cannot infer type from `$t` for `Rel` with no `model`'; + + // Else return as an acceptable type + return t; + } + + // Check if a typemirror is a collection + _isCollection(TypeMirror m) => m.isAssignableTo((reflectType(Iterable))); + // Check if a lib is scannable bool _isScannable(Uri uri) { String name = uri.toString(); diff --git a/test/datalayer/schema_test.dart b/test/datalayer/schema_test.dart index 8dd7073..b822ab3 100644 --- a/test/datalayer/schema_test.dart +++ b/test/datalayer/schema_test.dart @@ -46,6 +46,38 @@ main() { expect(Schema.get('NewUser').getField('ugly').column, equals('uglyThing')); expect(Schema.get('NewUser').getField('ugly').unique, equals(true)); }); + + test('Schema relation can be retrieved', () { + expect(Schema.get('Person').getRelation('addresses'), new isInstanceOf()); + }); + + test('Schema relation knows all the attributes', () { + SchemaRelation relation = Schema.get('Person').getRelation('addresses'); + expect(relation.name, equals('addresses')); + expect(relation.by, equals('externalId')); + expect(relation.as, equals('legacy_person_id')); + }); + + test('Schema relation infers `by` and `as` from Model', () { + SchemaRelation relation = Schema.get('Business').getRelation('addresses'); + expect(relation.by, equals('id')); + expect(relation.as, equals('business_id')); + }); + + test('Schema relation knows if it is a many or single association', () { + SchemaRelation relation = Schema.get('Business').getRelation('addresses'); + expect(relation.isCollection, equals(true)); + }); + + test('Schema with relation has derived field', () { + var schema = Schema.get('Address'); + var field = schema.getDerived('business_id'); + expect(field, new isInstanceOf()); + expect(field.primaryKey, equals(false)); + expect(field.index, equals(true)); + expect(field.column, equals(field.name)); + expect(field.type, equals('integer')); + }); }); } diff --git a/test/metadata/annotations_test.dart b/test/metadata/annotations_test.dart index 3283574..f373f1a 100644 --- a/test/metadata/annotations_test.dart +++ b/test/metadata/annotations_test.dart @@ -3,6 +3,8 @@ import 'package:test/test.dart'; import 'package:shellstone/shellstone.dart'; import '../test_setups.dart'; +// TODO: Is this suite of tests really required? The metadata set seems more +// useful?> main() { setUpAll(() async { // Start shellstone to setup any annotations diff --git a/test/metadata/metadata_test.dart b/test/metadata/metadata_test.dart index 40cc7d0..9e675dc 100644 --- a/test/metadata/metadata_test.dart +++ b/test/metadata/metadata_test.dart @@ -39,10 +39,27 @@ main() { expect(Metadata.name(user), equals('User')); }); - // Cant get this one to match for some reason - // test('Unknown Model type throws error', () { - // expect(Metadata.name(const Symbol('Explode')), throwsA(new isInstanceOf())); - // }); + test('Unknown Model type throws error', () { + expect(() => Metadata.name(const Symbol('Explode')), throwsA(new isInstanceOf())); + }); + + test('Metadata.rel(name) returns a Rel class', () { + expect(Metadata.rel('Person')['addresses'], new isInstanceOf()); + }); + + test('Rel model type can be retrieved', () { + expect(Metadata.rel('Person')['addresses'].model, equals(Address)); + }); + + test('Rel model type can be retrieved and inferred', () { + expect(Metadata.rel('Business')['addresses'].model, equals(Address)); + }); + + test('Rel members are captured', () { + Rel rel = Metadata.rel('Person')['addresses']; + expect(rel.by, equals('externalId')); + expect(rel.as, equals('legacy_person_id')); + }); }); } diff --git a/test/test_setups.dart b/test/test_setups.dart index 54a4bec..971295e 100644 --- a/test/test_setups.dart +++ b/test/test_setups.dart @@ -31,9 +31,23 @@ class PostgresUser { @Model(name: 'person', source: 'mongo') class Person { @Attr(type: 'integer', primaryKey: true) int id; + @Attr() int externalId; @Attr(type: 'string', column: 'FirstName') String firstName; @Attr(type: 'string', column: 'LastName') String lastName; @Attr(type: 'integer', column: 'Age') String age; + + @Rel(model: Address, by: 'externalId', as: 'legacy_person_id') List addresses; +} + +@Model() +class Business { + @Attr(primaryKey: true) int id; + @Rel() List
addresses; +} + +@Model() +class Address { + @Attr(primaryKey:true) int id; } @Adapter('mongo')