Line data Source code
1 : part of flutter_data;
2 :
3 : /// A mixin to "tag" and ensure the implementation of an [id] getter
4 : /// in data classes managed through Flutter Data.
5 : ///
6 : /// It contains private state and methods to track the model's identity.
7 : abstract class DataModel<T extends DataModel<T>> {
8 : Object? get id;
9 :
10 : // "late" finals
11 : String? _key;
12 : Map<String, RemoteAdapter>? _adapters;
13 :
14 : // computed
15 14 : String get _internalType => DataHelpers.getType<T>();
16 7 : RemoteAdapter<T> get remoteAdapter =>
17 21 : _adapters?[_internalType]! as RemoteAdapter<T>;
18 :
19 21 : bool get isInitialized => _key != null && _adapters != null;
20 :
21 : // initializers
22 :
23 7 : T _initialize(final Map<String, RemoteAdapter> adapters,
24 : {String? key, bool save = false}) {
25 7 : if (isInitialized) {
26 : if (save) {
27 : // ensure model is persisted, no need to notify
28 28 : remoteAdapter.localAdapter.save(_key!, this as T, notify: false);
29 : }
30 : return this as T;
31 : }
32 :
33 7 : _adapters = adapters;
34 :
35 49 : _key = remoteAdapter.graph.getKeyForId(remoteAdapter.internalType, id,
36 7 : keyIfAbsent: key ?? DataHelpers.generateKey<T>());
37 :
38 : if (save) {
39 28 : remoteAdapter.localAdapter.save(_key!, this as T);
40 : }
41 :
42 : // initialize relationships
43 : for (final metadata
44 34 : in remoteAdapter.localAdapter.relationshipsFor(this as T).entries) {
45 12 : final relationship = metadata.value['instance'] as Relationship?;
46 :
47 6 : relationship?.initialize(
48 : adapters: adapters,
49 : owner: this,
50 12 : name: metadata.value['name'] as String,
51 12 : inverseName: metadata.value['inverse'] as String?,
52 : );
53 : }
54 :
55 : return this as T;
56 : }
57 : }
58 :
59 : /// Extension that adds syntax-sugar to data classes,
60 : /// linking them to common [Repository] methods such as
61 : /// [save] and [delete].
62 : extension DataModelExtension<T extends DataModel<T>> on DataModel<T> {
63 : /// Initializes a model copying the identity of supplied [model].
64 : ///
65 : /// Usage:
66 : /// ```
67 : /// final post = await repository.findOne('1'); // returns initialized post
68 : /// final newPost = Post(title: 'test'); // uninitialized post
69 : /// newPost.was(post); // new is now initialized with same key as post
70 : /// ```
71 1 : T was(T model) {
72 : assert(model.isInitialized,
73 : 'Please initialize model before passing it to `was`');
74 3 : return _initialize(model._adapters!, key: model._key, save: true);
75 : }
76 :
77 : /// Saves this model through a call equivalent to [Repository.save].
78 : ///
79 : /// Usage: `await post.save()`, `author.save(remote: false, params: {'a': 'x'})`.
80 : ///
81 : /// **Requires this model to be initialized.**
82 2 : Future<T> save({
83 : bool? remote,
84 : Map<String, dynamic>? params,
85 : Map<String, String>? headers,
86 : OnData<T>? onSuccess,
87 : OnDataError<T>? onError,
88 : }) async {
89 1 : _assertInit('save');
90 3 : return await remoteAdapter.save(
91 : this as T,
92 : remote: remote,
93 : params: params,
94 : headers: headers,
95 : onSuccess: onSuccess,
96 : onError: onError,
97 : );
98 : }
99 :
100 : /// Deletes this model through a call equivalent to [Repository.delete].
101 : ///
102 : /// Usage: `await post.delete()`
103 : ///
104 : /// **Requires this model to be initialized.**
105 2 : Future<void> delete({
106 : bool? remote,
107 : Map<String, dynamic>? params,
108 : Map<String, String>? headers,
109 : OnData<void>? onSuccess,
110 : OnDataError<void>? onError,
111 : }) async {
112 1 : _assertInit('delete');
113 3 : await remoteAdapter.delete(
114 : this,
115 : remote: remote,
116 : params: params,
117 : headers: headers,
118 : onSuccess: onSuccess,
119 : onError: onError,
120 : );
121 : }
122 :
123 : /// Re-fetch this model through a call equivalent to [Repository.findOne].
124 : /// with the current object/[id]
125 : ///
126 : /// **Requires this model to be initialized.**
127 2 : Future<T?> reload({
128 : bool? remote,
129 : Map<String, dynamic>? params,
130 : Map<String, String>? headers,
131 : }) async {
132 1 : _assertInit('reload');
133 3 : return await remoteAdapter.findOne(
134 : this,
135 : remote: remote,
136 : params: params,
137 : headers: headers,
138 : );
139 : }
140 :
141 1 : void _assertInit(String method) {
142 1 : if (isInitialized) {
143 : return;
144 : }
145 1 : throw AssertionError('''\n
146 : This model MUST be initialized in order to call `$method`.
147 :
148 : DON'T DO THIS:
149 :
150 2 : final ${_internalType.singularize()} = $T(...);
151 2 : ${_internalType.singularize()}.$method(...);
152 :
153 : DO THIS:
154 :
155 2 : final ${_internalType.singularize()} = $T(...).init(ref.read);
156 2 : ${_internalType.singularize()}.$method(...);
157 :
158 : Call `init(ref.read)` on the model first.
159 :
160 : This ONLY happens when a model is manually instantiated
161 : and had no contact with Flutter Data.
162 :
163 : Initializing models is not necessary in any other case.
164 :
165 : When assigning new models to a relationship, only initialize
166 : the actual model:
167 :
168 : Family(surname: 'Carlson', dogs: {Dog(name: 'Jerry'), Dog(name: 'Zoe')}.asHasMany)
169 : .init(ref.read);
170 1 : ''');
171 : }
172 : }
173 :
174 : /// Returns a model's `_key` private attribute.
175 : ///
176 : /// Useful for testing, debugging or usage in [RemoteAdapter] subclasses.
177 2 : String? keyFor<T extends DataModel<T>>(T model) => model._key;
178 :
179 1 : @visibleForTesting
180 : @protected
181 : RemoteAdapter? adapterFor<T extends DataModel<T>>(T model) =>
182 1 : model.remoteAdapter;
|