@mittwald/api-models 0.0.0-development-04b7288-20240610

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.
Files changed (3) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +397 -0
  3. package/package.json +75 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Mittwald CM Service GmbH & Co. KG and contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,397 @@
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 to initialize an API client in order to operate with the models
24
+ provided by this package. Use the `api` global instance for initialization with
25
+ some methods.
26
+
27
+ ```typescript
28
+ import { api } from "@mittwald/api-models";
29
+
30
+ api.setupWithApiToken(process.env.MW_API_TOKEN);
31
+ ```
32
+
33
+ ## Examples
34
+
35
+ - A **`Reference`** or `ReferenceModel` represents a certain model just by its
36
+ ID.
37
+ - A **`DetailedModel`** contains all the data of the resource.
38
+
39
+ For a more detailed description refer to the section
40
+ [Type of models](#Type-of-models)
41
+
42
+ ```typescript
43
+ // Get a detailed project
44
+ const detailedProject = await Project.get("p-vka9t3");
45
+
46
+ // Create a project reference
47
+ const projectRef = Project.ofId("p-vka9t3");
48
+
49
+ // Get the detailed project from the reference
50
+ const anotherDetailedProject = await projectRef.getDetailed();
51
+
52
+ // Update project description
53
+ await detailedProject.updateDescription("My new description!");
54
+
55
+ // This method just needs the ID and a description and
56
+ // thus is also available on the reference
57
+ await projectRef.updateDescription("My new description!");
58
+
59
+ // Accessing the projects server reference
60
+ const server = project.server;
61
+
62
+ // List all projects of this server
63
+ const serversProjects = await server.listProjects();
64
+
65
+ // List all projects
66
+ const allProjects = await Project.list();
67
+
68
+ // Iterate over project List Models
69
+ for (const project of serversProjects) {
70
+ await project.leave();
71
+ }
72
+ ```
73
+
74
+ ## Usage in React
75
+
76
+ This package also provides methods aligned to be used in React components. It
77
+ uses
78
+ [@mittwald/react-use-promise](https://www.npmjs.com/package/@mittwald/react-use-promise)
79
+ to encapsulate all asynchronous functions into AsyncResources. More details
80
+ about how to use AsyncResources see the package documentation.
81
+
82
+ ### Installation
83
+
84
+ To use the React client you have to install the additional
85
+ `@mittwald/react-use-promise` package:
86
+
87
+ ```shell
88
+ yarn add @mittwald/react-use-promise
89
+ ```
90
+
91
+ All asynchronous methods provide a `use`-method property. This method uses
92
+ [@mittwald/react-use-promise](https://www.npmjs.com/package/@mittwald/react-use-promise)
93
+ under the hood to "resolve" the promise in the "React way".
94
+
95
+ ```typescript
96
+ const detailedProject = Project.get.use("p-vka9t3");
97
+
98
+ // Create a project reference
99
+ const projectRef = Project.ofId("p-vka9t3");
100
+
101
+ // Get the detailed project from the reference
102
+ const anotherDetailedProject = projectRef.getDetailed.use();
103
+
104
+ // Accessing the projects server reference
105
+ const server = project.server;
106
+
107
+ // List all projects of this server
108
+ const serversProjects = server.listProjects.use();
109
+
110
+ // List all projects
111
+ const allProjects = Project.list.use();
112
+ ```
113
+
114
+ ## Immutability and state updates
115
+
116
+ Most of all models provided by this package represent an associated counter-part
117
+ in the backend. When a model is loaded from the backend, the current state is
118
+ incorporated into the model instance. To keep it simple and predictable this
119
+ **state is immutable and does not change under any circumstances**. As a result
120
+ you must create a new instance to get an updated model and propagate it
121
+ throughout the runtime code.
122
+
123
+ This also applies for operations initiated at client-side. For example when the
124
+ `updateDescription` method is called on a project, the project instance will
125
+ still have the old description.
126
+
127
+ "Watching for changes" is not scope of this package and will be implemented in
128
+ future releases or other packages™️.
129
+
130
+ ## Contribute
131
+
132
+ **As a general advice when contributing, be sure to look at the existing source
133
+ code and use it as a template!**
134
+
135
+ Please consider the following conventions when adding or modifying models.
136
+
137
+ ### File structure
138
+
139
+ Structure the models in meaningful directories.
140
+
141
+ ### Use the base classes
142
+
143
+ Models should extend (or inherit) the correct base class. You can find the base
144
+ classes in `src/base`. The following classes are available.
145
+
146
+ #### `DataModel`
147
+
148
+ The DataModel is the foundation of all model classes that contain a set of
149
+ immutable generic data.
150
+
151
+ #### `ReferenceModel`
152
+
153
+ A ReferenceModel represents a certain model just by its ID. As the most basic
154
+ model operations often just need the ID and some input data (deleting, renaming,
155
+ ...), Reference Models can avoid unnecessary API round trips.
156
+
157
+ ### Stick to the ubiquitous language
158
+
159
+ When adding models or methods pay close attention to the (maybe existing)
160
+ language used in the respective domain. Talk to the responsible team if you are
161
+ uncertain.
162
+
163
+ ### Models as abstraction layer
164
+
165
+ Models should cover the following aspects:
166
+
167
+ - Coherent representation of the business logic
168
+ - Simple loading of models and their linked entities
169
+ - Methods to interact with the model in an intuitive way
170
+ - Preprocessing of the models raw data to increase DX
171
+ - Consistent API
172
+
173
+ ### Type of models
174
+
175
+ #### Detailed vs. List Models
176
+
177
+ The response type for some models often differs when loading single items or
178
+ lists. To reduce the amount of data, the list response type is usually a subset
179
+ of the comprehensive model. Add separate classes for the Detailed Model (name it
180
+ `[Model]Detailed`) and the List Model (name it `[Model]ListItem`).
181
+
182
+ If both model share a common code base, you should add a Common Model (name it
183
+ `[Model]Common`).
184
+
185
+ #### Reference Models
186
+
187
+ A Reference Model represents a certain model just by its ID. As the most basic
188
+ model operations often just need the ID and some input data (deleting, renaming,
189
+ ...), Reference Models can avoid unnecessary API round trips. These classes
190
+ should be used as a return type for newly created models or for linked models.
191
+
192
+ To get the actual Detailed Model, Reference Models _must_ have a
193
+ `function getDetailed(): Promise<ModelDetailed>` method.
194
+
195
+ Consider extending the Reference Model when implementing the Entry-Point Model.
196
+
197
+ #### Implementation details
198
+
199
+ When implementing shared functionality, like in the Common Models, you can use
200
+ the [`polytype`](https://www.npmjs.com/package/polytype) library to realize
201
+ dynamic multiple inheritance. Be sure to look at the existing source code for
202
+ implementation examples.
203
+
204
+ #### Entry-Point Model
205
+
206
+ Provide a single model (name it `[Model]`) as an entry point for all different
207
+ model types (detailed, list, ...). As a convention provide a default export for
208
+ this model.
209
+
210
+ ### Use the correct verbs
211
+
212
+ #### `find`
213
+
214
+ Entry-Point models should have a static `find` method. The find method returns
215
+ the detailed model or may return `undefined` if the model can not be found.
216
+
217
+ #### `get`
218
+
219
+ In addition to the `find` method Entry-Point models should have a static `get`
220
+ method. The get method should return the desired object or throw an
221
+ `ObjectNotFoundError`. You can use the `find` method and assert the existence
222
+ with the `assertObjectFound` function.
223
+
224
+ #### `list`
225
+
226
+ When a list of objects should be loaded use a `list` method. It may support a
227
+ `query` parameter to filter the result by given criteria.
228
+
229
+ #### `create`
230
+
231
+ When a model should be created use a static `create` method. This method should
232
+ return a reference of the created resource.
233
+
234
+ ### Accessing "linked" models
235
+
236
+ Most of the models are part of a larger model tree. Models should provide
237
+ methods to get the parent and child models, like `project.getServer()`,
238
+ `server.listProjects()` or `server.getCustomer()`. Use `get`, `list` or `find`
239
+ prefixes as described above.
240
+
241
+ #### Use Reference Models resp. Entry-Point Models when possible!
242
+
243
+ If a linked model provides a Reference Model or Entry-point Model, create it in
244
+ the model constructor, to avoid unnecessary API round trips.
245
+
246
+ ### Abstraction of model behaviors
247
+
248
+ Models are usually backed by a set of behaviors, defining the basic model
249
+ interactions. In order to actually "use" the model, it must be initialized with
250
+ a concrete behavior implementation. This layer of abstraction removes
251
+ implementation specific code from the model, and also makes behaviors
252
+ exchangeable without any impact on the model itself - for example inside unit
253
+ tests.
254
+
255
+ Consider using behaviors for:
256
+
257
+ - API interactions
258
+ - Storing and loading local data
259
+ - Complex computations or logic (maybe supported by an external library)
260
+
261
+ #### Encapsulate API interaction inside behaviors
262
+
263
+ Encapsulate any API interaction inside the model behaviors to prevent strong
264
+ coupling of model and API-specific implementation.
265
+
266
+ ##### Don't 🥴
267
+
268
+ ```typescript
269
+ class ProjectDetailed {
270
+ public static async find(
271
+ id: string,
272
+ ): Promise<ProjProjectDetailed | undefined> {
273
+ const response = await client.project.getProject({
274
+ id,
275
+ });
276
+
277
+ if (response.status === 200) {
278
+ return new Project(response.data.id, response.data);
279
+ }
280
+ assertStatus(response, 403);
281
+ }
282
+ }
283
+ ```
284
+
285
+ ##### Do 😃
286
+
287
+ ```typescript
288
+ class ProjectDetailed {
289
+ public static async find(id: string): Promise<ProjectDetailed | undefined> {
290
+ const data = await config.project.behaviors.find(id);
291
+ if (data !== undefined) {
292
+ return new Project(data.id, data);
293
+ }
294
+ }
295
+ }
296
+ ```
297
+
298
+ #### How-to implement behaviors
299
+
300
+ Place a `behaviors` folder inside the model that should look like this:
301
+
302
+ ```
303
+ Project/
304
+ ├─ behaviors/
305
+ │ ├─ index.ts
306
+ │ ├─ types.ts (behavior interface)
307
+ │ ├─ api.ts (behavior implementation)
308
+ │ ├─ inmem.ts (behavior implementation)
309
+
310
+ ```
311
+
312
+ ##### Define `types.ts` first
313
+
314
+ It is a good starting point to first implement the interface for the behavior.
315
+ The interface usually just defines methods used in the behavior. Like
316
+
317
+ ```ts
318
+ export interface ProjectBehaviors {
319
+ find: (id: string) => Promise<ProjectData | undefined>;
320
+ updateDescription: (projectId: string, description: string) => Promise<void>;
321
+ }
322
+ ```
323
+
324
+ Then register the behavior in the global behavior configuration
325
+ `packages/models/src/config/config.ts`.
326
+
327
+ ##### Use the behaviors in the model
328
+
329
+ If the behavior interface is defined, you can start implementing the model. You
330
+ can also first implement the concrete API behavior, to "proof" the behavior is
331
+ "working" with the real API.
332
+
333
+ ```ts
334
+ import { config } from "../../config/config.js";
335
+
336
+ class ProjectDetailed {
337
+ public static async find(id: string): Promise<ProjectDetailed | undefined> {
338
+ const data = await config.project.behaviors.find(id);
339
+ if (data !== undefined) {
340
+ return new Project(data.id, data);
341
+ }
342
+ }
343
+ }
344
+ ```
345
+
346
+ ##### Implement the API behavior
347
+
348
+ The API behavior depends on an API client. You can implement the behavior as an
349
+ object factory, or a simple class implementing the interface. When using the
350
+ object factory, you do not have to redeclare the method parameter types.
351
+
352
+ Do the implementation specific stuff, thus preparing and executing the request,
353
+ and finally processing the response.
354
+
355
+ ```ts
356
+ import { ProjectBehaviors } from "./types.js";
357
+ import { assertStatus, MittwaldAPIV2Client } from "@mittwald/api-client";
358
+
359
+ export const apiProjectBehaviors = (
360
+ client: MittwaldAPIV2Client,
361
+ ): ProjectBehaviors => ({
362
+ find: async (id) => {
363
+ const response = await client.project.getProject({
364
+ projectId: id,
365
+ });
366
+
367
+ if (response.status === 200) {
368
+ return response.data;
369
+ }
370
+ assertStatus(response, 403);
371
+ },
372
+ });
373
+ ```
374
+
375
+ ### Prepare for React
376
+
377
+ All asynchronous methods should provide a `use`-method property. This method
378
+ uses
379
+ [@mittwald/react-use-promise](https://www.npmjs.com/package/@mittwald/react-use-promise)
380
+ under the hood to "resolve" the promise in the "React way".
381
+
382
+ To provide this feature to your _async_ model methods, wrap the actual method
383
+ with the `provideReact` enhancer.
384
+
385
+ ```ts
386
+ class ProjectDetailed {
387
+ public static find = provideReact(
388
+ async (id: string): Promise<ProjectDetailed | undefined> => {
389
+ const data = await config.behaviors.project.find(id);
390
+
391
+ if (data !== undefined) {
392
+ return new ProjectDetailed(data);
393
+ }
394
+ },
395
+ );
396
+ }
397
+ ```
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@mittwald/api-models",
3
+ "version": "0.0.0-development-04b7288-20240610",
4
+ "author": "Mittwald CM Service GmbH & Co. KG <opensource@mittwald.de>",
5
+ "type": "module",
6
+ "description": "Collection of domain models for coherent interaction with the API",
7
+ "keywords": [
8
+ "api",
9
+ "client",
10
+ "mittwald",
11
+ "rest",
12
+ "sdk"
13
+ ],
14
+ "homepage": "https://developer.mittwald.de",
15
+ "repository": "github:mittwald/api-client-js",
16
+ "bugs": {
17
+ "url": "https://github.com/mittwald/api-client-js/issues"
18
+ },
19
+ "license": "MIT",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/types/index.d.ts",
23
+ "import": "./dist/esm/index.js"
24
+ },
25
+ "./react": {
26
+ "types": "./dist/types/react.d.ts",
27
+ "import": "./dist/esm/react.js"
28
+ }
29
+ },
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "scripts": {
34
+ "build": "run build:clean && run tsc",
35
+ "build:clean": "rimraf dist",
36
+ "lint": "run eslint .",
37
+ "test": "node --experimental-vm-modules $(yarn bin jest)"
38
+ },
39
+ "dependencies": {
40
+ "@mittwald/api-client": "^0.0.0-development-04b7288-20240610",
41
+ "another-deep-freeze": "^1.0.0",
42
+ "polytype": "^0.17.0",
43
+ "type-fest": "^4.12.0"
44
+ },
45
+ "devDependencies": {
46
+ "@jest/globals": "^29.7.0",
47
+ "@mittwald/react-use-promise": "^2.3.12",
48
+ "@types/jest": "^29.5.12",
49
+ "@types/react": "^18.2.64",
50
+ "@typescript-eslint/eslint-plugin": "^7.1.1",
51
+ "@typescript-eslint/parser": "^7.1.1",
52
+ "eslint": "^8.57.0",
53
+ "eslint-config-prettier": "^9.1.0",
54
+ "eslint-plugin-json": "^3.1.0",
55
+ "eslint-plugin-prettier": "^5.1.3",
56
+ "jest": "^29.7.0",
57
+ "prettier": "^3.2.5",
58
+ "react": "^18.2.0",
59
+ "rimraf": "^5.0.5",
60
+ "ts-jest": "^29.1.2",
61
+ "typescript": "^5.4.2"
62
+ },
63
+ "peerDependencies": {
64
+ "@mittwald/react-use-promise": "^2.3.12"
65
+ },
66
+ "peerDependenciesMeta": {
67
+ "@mittwald/react-use-promise": {
68
+ "optional": true
69
+ },
70
+ "react": {
71
+ "optional": true
72
+ }
73
+ },
74
+ "gitHead": "97660b1fee614cc23b778573ba3e8ae75a961517"
75
+ }