@mittwald/api-models 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,300 @@
1
+ # mittwald API models
2
+
3
+ This package contains a collection of domain models for coherent interaction
4
+ with the mittwald API.
5
+
6
+ ## License
7
+
8
+ Copyright (c) 2023 Mittwald CM Service GmbH & Co. KG and contributors
9
+
10
+ This project (and all NPM packages) therein is licensed under the MIT License;
11
+ see the [LICENSE](../../LICENSE) file for details.
12
+
13
+ ## Installing
14
+
15
+ You can install this package from the regular NPM registry:
16
+
17
+ ```shell
18
+ yarn add @mittwald/api-models
19
+ ```
20
+
21
+ ## Setup
22
+
23
+ You will need an initialized API client in order to operate with the models
24
+ provided by this package. Provide it to the `setupApiBehaviors` method before
25
+ any model operation is executed.
26
+
27
+ ```typescript
28
+ import { setupApiBehaviors } from "@mittwald/api-models";
29
+ import { MittwaldAPIV2Client } from "@mittwald/api-client";
30
+
31
+ // See the documentation of @mittwald/api-client for alternative methods of
32
+ // client initialization: https://www.npmjs.com/package/@mittwald/api-client
33
+ const client = MittwaldAPIClient.newUnauthenticated();
34
+ setupApiBehaviors(client);
35
+ ```
36
+
37
+ ## Examples
38
+
39
+ - A **`Reference`** or `ReferenceModel` represents a certain model just by its
40
+ ID.
41
+ - A **`DetailedModel`** contains all the data of the resource.
42
+
43
+ For a more detailed description refer to the section
44
+ [Type of models](#Type-of-models)
45
+
46
+ ```typescript
47
+ // Get a detailed project
48
+ const detailedProject = await Project.get("p-vka9t3");
49
+
50
+ // Create a project reference
51
+ const projectRef = Project.ofId("p-vka9t3");
52
+
53
+ // Get the detailed project from the reference
54
+ const anotherDetailedProject = await projectRef.getDetailed();
55
+
56
+ // Update project description
57
+ await detailedProject.updateDescription("My new description!");
58
+
59
+ // This method just needs the ID and a description and
60
+ // thus is also available on the reference
61
+ await projectRef.updateDescription("My new description!");
62
+
63
+ // Accessing the projects server reference
64
+ const server = project.server;
65
+
66
+ // List all projects of this server
67
+ const serversProjects = await server.listProjects();
68
+
69
+ // List all projects
70
+ const allProjects = await Project.list();
71
+
72
+ // Iterate over project List Models
73
+ for (const project of serversProjects) {
74
+ await project.leave();
75
+ }
76
+ ```
77
+
78
+ ## Usage in React
79
+
80
+ This package also provides methods aligned to be used in React components. It
81
+ uses
82
+ [@mittwald/react-use-promise](https://www.npmjs.com/package/@mittwald/react-use-promise)
83
+ to encapsulate all asynchronous functions into AsyncResources. More details
84
+ about how to use AsyncResources see the package documentation.
85
+
86
+ ### Installation
87
+
88
+ To use the React client you have to install the additional
89
+ `@mittwald/react-use-promise` package:
90
+
91
+ ```shell
92
+ yarn add @mittwald/react-use-promise
93
+ ```
94
+
95
+ All asynchronous methods provide a `use`-method property. This method uses
96
+ [@mittwald/react-use-promise](https://www.npmjs.com/package/@mittwald/react-use-promise)
97
+ under the hood to "resolve" the promise in the "React way".
98
+
99
+ ```typescript
100
+ const detailedProject = Project.get.use("p-vka9t3");
101
+
102
+ // Create a project reference
103
+ const projectRef = Project.ofId("p-vka9t3");
104
+
105
+ // Get the detailed project from the reference
106
+ const anotherDetailedProject = projectRef.getDetailed.use();
107
+
108
+ // Accessing the projects server reference
109
+ const server = project.server;
110
+
111
+ // List all projects of this server
112
+ const serversProjects = server.listProjects.use();
113
+
114
+ // List all projects
115
+ const allProjects = Project.list.use();
116
+ ```
117
+
118
+ ## Immutability and state updates
119
+
120
+ Most of all models provided by this package represent an associated counter-part
121
+ in the backend. When a model is loaded from the backend, the current state is
122
+ incorporated into the model instance. To keep it simple and predictable this
123
+ **state is immutable and does not change under any circumstances**. As a result
124
+ you must create a new instance to get an updated model and propagate it
125
+ throughout the runtime code.
126
+
127
+ This also applies for operations initiated at client-side. For example when the
128
+ `updateDescription` method is called on a project, the project instance will
129
+ still have the old description.
130
+
131
+ "Watching for changes" is not scope of this package and will be implemented in
132
+ future releases or other packages™️.
133
+
134
+ ## Contribute
135
+
136
+ **As a general advice when contributing, be sure to look at the existing source
137
+ code and use it as a template!**
138
+
139
+ Please consider the following conventions when adding or modifying models.
140
+
141
+ ### File structure
142
+
143
+ Structure the models in meaningful directories.
144
+
145
+ ### Use the base classes
146
+
147
+ Models should extend (or inherit) the correct base class. You can find the base
148
+ classes in `src/base`. The following classes are available.
149
+
150
+ #### `DataModel`
151
+
152
+ The DataModel is the foundation of all model classes that contain a set of
153
+ immutable generic data.
154
+
155
+ #### `ReferenceModel`
156
+
157
+ A ReferenceModel represents a certain model just by its ID. As the most basic
158
+ model operations often just need the ID and some input data (deleting, renaming,
159
+ ...), Reference Models can avoid unnecessary API round trips.
160
+
161
+ ### Stick to the ubiquitous language
162
+
163
+ When adding models or methods pay close attention to the (maybe existing)
164
+ language used in the respective domain. Talk to the responsible team if you are
165
+ uncertain.
166
+
167
+ ### Models as abstraction layer
168
+
169
+ Models should cover the following aspects:
170
+
171
+ - Coherent representation of the business logic
172
+ - Simple loading of models and their linked entities
173
+ - Methods to interact with the model in an intuitive way
174
+ - Preprocessing of the models raw data to increase DX
175
+ - Consistent API
176
+
177
+ ### Type of models
178
+
179
+ #### Detailed vs. List Models
180
+
181
+ The response type for some models often differs when loading single items or
182
+ lists. To reduce the amount of data, the list response type is usually a subset
183
+ of the comprehensive model. Add separate classes for the Detailed Model (name it
184
+ `[Model]Detailed`) and the List Model (name it `[Model]ListItem`).
185
+
186
+ If both model share a common code base, you should add a Common Model (name it
187
+ `[Model]Common`).
188
+
189
+ #### Reference Models
190
+
191
+ A Reference Model represents a certain model just by its ID. As the most basic
192
+ model operations often just need the ID and some input data (deleting, renaming,
193
+ ...), Reference Models can avoid unnecessary API round trips. These classes
194
+ should be used as a return type for newly created models or for linked models.
195
+
196
+ To get the actual Detailed Model, Reference Models _must_ have a
197
+ `function getDetailed(): Promise<ModelDetailed>` method.
198
+
199
+ Consider extending the Reference Model when implementing the Entry-Point Model.
200
+
201
+ #### Implementation details
202
+
203
+ When implementing shared functionality, like in the Common Models, you can use
204
+ the [`polytype`](https://www.npmjs.com/package/polytype) library to realize
205
+ dynamic multiple inheritance. Be sure to look at the existing source code for
206
+ implementation examples.
207
+
208
+ #### Entry-Point Model
209
+
210
+ Provide a single model (name it `[Model]`) as an entry point for all different
211
+ model types (detailed, list, ...). As a convention provide a default export for
212
+ this model.
213
+
214
+ ### Use the correct verbs
215
+
216
+ #### `find`
217
+
218
+ Entry-Point models should have a static `find` method. The find method returns
219
+ the detailed model or may return `undefined` if the model can not be found.
220
+
221
+ #### `get`
222
+
223
+ In addition to the `find` method Entry-Point models should have a static `get`
224
+ method. The get method should return the desired object or throw an
225
+ `ObjectNotFoundError`. You can use the `find` method and assert the existence
226
+ with the `assertObjectFound` function.
227
+
228
+ #### `list`
229
+
230
+ When a list of objects should be loaded use a `list` method. It may support a
231
+ `query` parameter to filter the result by given criteria.
232
+
233
+ #### `create`
234
+
235
+ When a model should be created use a static `create` method. This method should
236
+ return a reference of the created resource.
237
+
238
+ ### Accessing "linked" models
239
+
240
+ Most of the models are part of a larger model tree. Models should provide
241
+ methods to get the parent and child models, like `project.getServer()`,
242
+ `server.listProjects()` or `server.getCustomer()`. Use `get`, `list` or `find`
243
+ prefixes as described above.
244
+
245
+ #### Use Reference Models resp. Entry-Point Models when possible!
246
+
247
+ If a linked model provides a Reference Model or Entry-point Model, create it in
248
+ the model constructor, to avoid unnecessary API round trips.
249
+
250
+ ### Abstraction of model behaviors
251
+
252
+ Models are usually backed by a set of behaviors, defining the basic model
253
+ interactions. In order to actually "use" the model, it must be initialized with
254
+ a concrete behavior implementation. This layer of abstraction removes
255
+ implementation specific code from the model, and also makes behaviors
256
+ exchangeable without any impact on the model itself - for example inside unit
257
+ tests.
258
+
259
+ Consider using behaviors for:
260
+
261
+ - API interactions
262
+ - Storing and loading local data
263
+ - Complex computations or logic (maybe supported by an external library)
264
+
265
+ #### Encapsulate API interaction inside behaviors
266
+
267
+ Encapsulate any API interaction inside the model behaviors to prevent strong
268
+ coupling of model and API-specific implementation.
269
+
270
+ ##### Don't 🥴
271
+
272
+ ```typescript
273
+ class ProjectDetailed {
274
+ public static async find(
275
+ id: string,
276
+ ): Promise<ProjProjectDetailed | undefined> {
277
+ const response = await client.project.getProject({
278
+ id,
279
+ });
280
+
281
+ if (response.status === 200) {
282
+ return new Project(response.data.id, response.data);
283
+ }
284
+ assertStatus(response, 403);
285
+ }
286
+ }
287
+ ```
288
+
289
+ ##### Do 😃
290
+
291
+ ```typescript
292
+ class ProjectDetailed {
293
+ public static async find(id: string): Promise<ProjectDetailed | undefined> {
294
+ const data = await Project.behaviors.find(id);
295
+ if (data !== undefined) {
296
+ return new Project(data.id, data);
297
+ }
298
+ }
299
+ }
300
+ ```