blob: de895bc2264ff544e36420ca268919867a54668b [file] [log] [blame]
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
part of '../../db.dart';
/// Annotation used to mark dart classes which can be stored into datastore.
///
/// The `Kind` annotation on a class as well as other `Property` annotations on
/// fields or getters of the class itself (and any of it's superclasses) up to
/// the [Model] class describe the *mapping* of *dart objects* to datastore
/// *entities*.
///
/// An "entity" is an object which can be stored into Google Cloud Datastore.
/// It contains a number of named "properties", some of them might get indexed,
/// others are not. A "property" value can be of a limited set of supported
/// types (such as `int` and `String`).
///
/// Here is an example of a dart model class which can be stored into datastore:
/// @Kind()
/// class Person extends db.Model {
/// @StringProperty()
/// String name;
///
/// @IntProperty()
/// int age;
///
/// @DateTimeProperty()
/// DateTime dateOfBirth;
/// }
class Kind {
/// The kind name used when saving objects to datastore.
///
/// If `null` the name will be the same as the class name at which the
/// annotation is placed.
final String? name;
/// The type, either [ID_TYPE_INTEGER] or [ID_TYPE_STRING].
final IdType idType;
/// Annotation specifying the name of this kind and whether to use integer or
/// string `id`s.
///
/// If `name` is omitted, it will default to the name of class to which this
/// annotation is attached to.
const Kind({this.name, this.idType = IdType.Integer});
}
/// The type used for id's of an entity.
class IdType {
/// Use integer ids for identifying entities.
// ignore: constant_identifier_names
static const IdType Integer = IdType('Integer');
/// Use string ids for identifying entities.
// ignore: constant_identifier_names
static const IdType String = IdType('String');
final core.String _type;
const IdType(this._type);
@override
core.String toString() => 'IdType: $_type';
}
/// Describes a property of an Entity.
///
/// Please see [Kind] for an example on how to use them.
abstract class Property {
/// The name of the property.
///
/// If it is `null`, the name will be the same as used in the
/// model class.
final String? propertyName;
/// Specifies whether this property is required or not.
///
/// If required is `true`, it will be enforced when saving model objects to
/// the datastore and when retrieving them.
final bool required;
/// Specifies whether this property should be indexed or not.
///
/// When running queries no this property, it is necessary to set [indexed] to
/// `true`.
final bool indexed;
const Property(
{this.propertyName, this.required = false, this.indexed = true});
bool validate(ModelDB db, Object? value) {
if (required && value == null) return false;
return true;
}
Object? encodeValue(ModelDB db, Object? value, {bool forComparison = false});
Object? decodePrimitiveValue(ModelDB db, Object? value);
}
/// An abstract base class for primitive properties which can e.g. be used
/// within a composed `ListProperty`.
abstract class PrimitiveProperty extends Property {
const PrimitiveProperty(
{String? propertyName, bool required = false, bool indexed = true})
: super(propertyName: propertyName, required: required, indexed: indexed);
@override
Object? encodeValue(ModelDB db, Object? value,
{bool forComparison = false}) =>
value;
@override
Object? decodePrimitiveValue(ModelDB db, Object? value) => value;
}
/// A boolean [Property].
///
/// It will validate that values are booleans before writing them to the
/// datastore and when reading them back.
class BoolProperty extends PrimitiveProperty {
const BoolProperty(
{String? propertyName, bool required = false, bool indexed = true})
: super(propertyName: propertyName, required: required, indexed: indexed);
@override
bool validate(ModelDB db, Object? value) =>
super.validate(db, value) && (value == null || value is bool);
}
/// A integer [Property].
///
/// It will validate that values are integers before writing them to the
/// datastore and when reading them back.
class IntProperty extends PrimitiveProperty {
const IntProperty(
{String? propertyName, bool required = false, bool indexed = true})
: super(propertyName: propertyName, required: required, indexed: indexed);
@override
bool validate(ModelDB db, Object? value) =>
super.validate(db, value) && (value == null || value is int);
}
/// A double [Property].
///
/// It will validate that values are doubles before writing them to the
/// datastore and when reading them back.
class DoubleProperty extends PrimitiveProperty {
const DoubleProperty(
{String? propertyName, bool required = false, bool indexed = true})
: super(propertyName: propertyName, required: required, indexed: indexed);
@override
bool validate(ModelDB db, Object? value) =>
super.validate(db, value) && (value == null || value is double);
}
/// A string [Property].
///
/// It will validate that values are strings before writing them to the
/// datastore and when reading them back.
class StringProperty extends PrimitiveProperty {
const StringProperty(
{String? propertyName, bool required = false, bool indexed = true})
: super(propertyName: propertyName, required: required, indexed: indexed);
@override
bool validate(ModelDB db, Object? value) =>
super.validate(db, value) && (value == null || value is String);
}
/// A key [Property].
///
/// It will validate that values are keys before writing them to the
/// datastore and when reading them back.
class ModelKeyProperty extends PrimitiveProperty {
const ModelKeyProperty(
{String? propertyName, bool required = false, bool indexed = true})
: super(propertyName: propertyName, required: required, indexed: indexed);
@override
bool validate(ModelDB db, Object? value) =>
super.validate(db, value) && (value == null || value is Key);
@override
Object? encodeValue(ModelDB db, Object? value, {bool forComparison = false}) {
if (value == null) return null;
return db.toDatastoreKey(value as Key);
}
@override
Object? decodePrimitiveValue(ModelDB db, Object? value) {
if (value == null) return null;
return db.fromDatastoreKey(value as ds.Key);
}
}
/// A binary blob [Property].
///
/// It will validate that values are blobs before writing them to the
/// datastore and when reading them back. Blob values will be represented by
/// List<int>.
class BlobProperty extends PrimitiveProperty {
const BlobProperty({String? propertyName, bool required = false})
: super(propertyName: propertyName, required: required, indexed: false);
// NOTE: We don't validate that the entries of the list are really integers
// of the range 0..255!
// If an untyped list was created the type check will always succeed. i.e.
// "[1, true, 'bar'] is List<int>" evaluates to `true`
@override
bool validate(ModelDB db, Object? value) =>
super.validate(db, value) && (value == null || value is List<int>);
@override
Object? encodeValue(ModelDB db, Object? value, {bool forComparison = false}) {
if (value == null) return null;
return ds.BlobValue(value as List<int>);
}
@override
Object? decodePrimitiveValue(ModelDB db, Object? value) {
if (value == null) return null;
return (value as ds.BlobValue).bytes;
}
}
/// A datetime [Property].
///
/// It will validate that values are DateTime objects before writing them to the
/// datastore and when reading them back.
class DateTimeProperty extends PrimitiveProperty {
const DateTimeProperty(
{String? propertyName, bool required = false, bool indexed = true})
: super(propertyName: propertyName, required: required, indexed: indexed);
@override
bool validate(ModelDB db, Object? value) =>
super.validate(db, value) && (value == null || value is DateTime);
@override
Object? decodePrimitiveValue(ModelDB db, Object? value) {
if (value is int) {
return DateTime.fromMillisecondsSinceEpoch(value ~/ 1000, isUtc: true);
}
return value;
}
}
/// A composed list [Property], with a `subProperty` for the list elements.
///
/// It will validate that values are List objects before writing them to the
/// datastore and when reading them back. It will also validate the elements
/// of the list itself.
class ListProperty extends Property {
final PrimitiveProperty subProperty;
// TODO: We want to support optional list properties as well.
// Get rid of "required: true" here.
const ListProperty(this.subProperty,
{String? propertyName, bool indexed = true})
: super(propertyName: propertyName, required: true, indexed: indexed);
@override
bool validate(ModelDB db, Object? value) {
if (!super.validate(db, value) || value is! List) return false;
for (var entry in value) {
if (!subProperty.validate(db, entry)) return false;
}
return true;
}
@override
Object? encodeValue(ModelDB db, Object? value, {bool forComparison = false}) {
if (forComparison) {
// If we have comparison of list properties (i.e. repeated property names)
// the comparison object must not be a list, but the value itself.
// i.e.
//
// class Article {
// ...
// @ListProperty(StringProperty())
// List<String> tags;
// ...
// }
//
// should be queried via
//
// await db.query(Article, 'tags=', "Dart").toList();
//
// So the [value] for the comparison is of type `String` and not
// `List<String>`!
return subProperty.encodeValue(db, value, forComparison: true);
}
if (value == null) return null;
var list = value as List;
if (list.isEmpty) return null;
if (list.length == 1) return subProperty.encodeValue(db, list[0]);
return list.map((value) => subProperty.encodeValue(db, value)).toList();
}
@override
Object decodePrimitiveValue(ModelDB db, Object? value) {
if (value == null) return [];
if (value is! List) return [subProperty.decodePrimitiveValue(db, value)];
return value
.map((entry) => subProperty.decodePrimitiveValue(db, entry))
.toList();
}
}
/// A convenience [Property] for list of strings.
class StringListProperty extends ListProperty {
const StringListProperty({String? propertyName, bool indexed = true})
: super(const StringProperty(),
propertyName: propertyName, indexed: indexed);
@override
Object decodePrimitiveValue(ModelDB db, Object? value) {
return (super.decodePrimitiveValue(db, value) as core.List).cast<String>();
}
}