@mittwald/api-models 0.0.0-development-74e1fd1-20240611 → 0.0.0-development-e096ee7-20240924

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 (53) hide show
  1. package/README.md +55 -11
  2. package/dist/esm/app/AppInstallation/AppInstallation.js +48 -4
  3. package/dist/esm/app/AppInstallation/behaviors/api.js +5 -2
  4. package/dist/esm/base/ListDataModel.js +8 -0
  5. package/dist/esm/base/ListQueryModel.js +9 -0
  6. package/dist/esm/base/index.js +5 -0
  7. package/dist/esm/config/behaviors/api.js +3 -0
  8. package/dist/esm/config/config.js +1 -0
  9. package/dist/esm/customer/Customer/Customer.js +59 -6
  10. package/dist/esm/customer/Customer/behaviors/api.js +12 -2
  11. package/dist/esm/domain/Ingress/Ingress.js +56 -6
  12. package/dist/esm/domain/Ingress/behaviors/api.js +7 -5
  13. package/dist/esm/project/Project/Project.js +57 -5
  14. package/dist/esm/project/Project/behaviors/api.js +5 -2
  15. package/dist/esm/project/Project/behaviors/inMem.js +5 -1
  16. package/dist/esm/react/MittwaldApiModelProvider.js +8 -8
  17. package/dist/esm/react/asyncResourceInvalidation.js +29 -0
  18. package/dist/esm/react/index.js +2 -1
  19. package/dist/esm/react/provideReact.js +24 -0
  20. package/dist/esm/react/provideReact.test-types.js +21 -0
  21. package/dist/esm/react/reactProvisionContext.js +2 -0
  22. package/dist/esm/server/Server/Server.js +68 -13
  23. package/dist/esm/server/Server/behaviors/api.js +5 -2
  24. package/dist/types/app/AppInstallation/AppInstallation.d.ts +26 -17
  25. package/dist/types/app/AppInstallation/behaviors/types.d.ts +3 -2
  26. package/dist/types/app/AppInstallation/types.d.ts +1 -1
  27. package/dist/types/base/ListDataModel.d.ts +5 -0
  28. package/dist/types/base/ListQueryModel.d.ts +5 -0
  29. package/dist/types/base/index.d.ts +5 -0
  30. package/dist/types/base/types.d.ts +4 -0
  31. package/dist/types/config/config.d.ts +1 -0
  32. package/dist/types/customer/Customer/Customer.d.ts +34 -21
  33. package/dist/types/customer/Customer/behaviors/types.d.ts +4 -2
  34. package/dist/types/customer/Customer/types.d.ts +2 -1
  35. package/dist/types/domain/Ingress/Ingress.d.ts +24 -17
  36. package/dist/types/domain/Ingress/behaviors/types.d.ts +3 -2
  37. package/dist/types/domain/Ingress/types.d.ts +5 -3
  38. package/dist/types/project/Project/Project.d.ts +78 -68
  39. package/dist/types/project/Project/behaviors/types.d.ts +3 -2
  40. package/dist/types/project/Project/types.d.ts +7 -1
  41. package/dist/types/react/MittwaldApiModelProvider.d.ts +6 -2
  42. package/dist/types/react/asyncResourceInvalidation.d.ts +4 -0
  43. package/dist/types/react/index.d.ts +3 -2
  44. package/dist/types/react/provideReact.d.ts +9 -0
  45. package/dist/types/react/provideReact.test-types.d.ts +1 -0
  46. package/dist/types/react/reactProvisionContext.d.ts +3 -0
  47. package/dist/types/react/reactUsePromise.d.ts +1 -0
  48. package/dist/types/server/Server/Server.d.ts +71 -28
  49. package/dist/types/server/Server/behaviors/types.d.ts +3 -2
  50. package/dist/types/server/Server/types.d.ts +5 -1
  51. package/package.json +26 -15
  52. package/dist/esm/lib/provideReact.js +0 -5
  53. package/dist/types/lib/provideReact.d.ts +0 -11
package/README.md CHANGED
@@ -60,10 +60,10 @@ await projectRef.updateDescription("My new description!");
60
60
  const server = project.server;
61
61
 
62
62
  // List all projects of this server
63
- const serversProjects = await server.listProjects();
63
+ const serversProjects = await server.projects.execute();
64
64
 
65
65
  // List all projects
66
- const allProjects = await Project.list();
66
+ const allProjects = await Project.query().execute();
67
67
 
68
68
  // Iterate over project List Models
69
69
  for (const project of serversProjects) {
@@ -105,10 +105,10 @@ const anotherDetailedProject = projectRef.getDetailed.use();
105
105
  const server = project.server;
106
106
 
107
107
  // List all projects of this server
108
- const serversProjects = server.listProjects.use();
108
+ const serversProjects = server.projects.execute.use();
109
109
 
110
110
  // List all projects
111
- const allProjects = Project.list.use();
111
+ const allProjects = Project.query().execute.use();
112
112
  ```
113
113
 
114
114
  ## Immutability and state updates
@@ -190,10 +190,55 @@ model operations often just need the ID and some input data (deleting, renaming,
190
190
  should be used as a return type for newly created models or for linked models.
191
191
 
192
192
  To get the actual Detailed Model, Reference Models _must_ have a
193
- `function getDetailed(): Promise<ModelDetailed>` method.
193
+ `function getDetailed(): Promise<ModelDetailed>` and
194
+ `function findDetailed(): Promise<ModelDetailed|undefined>` method.
194
195
 
195
196
  Consider extending the Reference Model when implementing the Entry-Point Model.
196
197
 
198
+ #### Query Models
199
+
200
+ Querying models usually requires a query object – or short query. The query
201
+ mostly includes pagination settings like `limit`, `skip` or `page`. It may also
202
+ include filters like `fromDate` or `toDate`, and filters to other models like
203
+ `customerId`.
204
+
205
+ A Query Model represents a specific query to a specific model and should include
206
+ the following methods:
207
+
208
+ - `execute()`: executes the query and returns the respective List Model
209
+ - `refine(overrideQuery)`: creates a new Query Model with a refined query object
210
+ - `getTotalCount()`: gets the total count of the query (executes the query with
211
+ `limit: 0`)
212
+
213
+ When a model supports queries, it should provide a static `query()` method to
214
+ create the respective Query Model.
215
+
216
+ When a model is used as a query parameters in a Query Model, the model should
217
+ have a property in its Reference Model for this Query Model. See the following
218
+ example:
219
+
220
+ ```typescript
221
+ class Server {
222
+ public readonly projects: ProjectsListQuery;
223
+
224
+ public constructor(id: string) {
225
+ this.projects = new ProjectListQuery({
226
+ server: this,
227
+ });
228
+ }
229
+ }
230
+ ```
231
+
232
+ #### List Models
233
+
234
+ List Models are the result of a Query Model execution. A List Model includes
235
+
236
+ - a list of the respective List Models, limited by the pagination configuration
237
+ - the available total count (useful to implement pagination or count data)
238
+
239
+ List Models should extend their respective Query Model, because it might be
240
+ helpful to also call `refine()` on an already executed query.
241
+
197
242
  #### Implementation details
198
243
 
199
244
  When implementing shared functionality, like in the Common Models, you can use
@@ -204,7 +249,7 @@ implementation examples.
204
249
  #### Entry-Point Model
205
250
 
206
251
  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
252
+ model types (detailed, query, ...). As a convention provide a default export for
208
253
  this model.
209
254
 
210
255
  ### Use the correct verbs
@@ -221,9 +266,9 @@ method. The get method should return the desired object or throw an
221
266
  `ObjectNotFoundError`. You can use the `find` method and assert the existence
222
267
  with the `assertObjectFound` function.
223
268
 
224
- #### `list`
269
+ #### `query`
225
270
 
226
- When a list of objects should be loaded use a `list` method. It may support a
271
+ When a list of objects should be queried use a `query` method. It may support a
227
272
  `query` parameter to filter the result by given criteria.
228
273
 
229
274
  #### `create`
@@ -234,9 +279,8 @@ return a reference of the created resource.
234
279
  ### Accessing "linked" models
235
280
 
236
281
  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.
282
+ properties to get the parent and child models, like `project.server`,
283
+ `server.projects` or `server.customer`.
240
284
 
241
285
  #### Use Reference Models resp. Entry-Point Models when possible!
242
286
 
@@ -3,7 +3,10 @@ import { classes } from "polytype";
3
3
  import { DataModel } from "../../base/DataModel.js";
4
4
  import assertObjectFound from "../../base/assertObjectFound.js";
5
5
  import { ReferenceModel } from "../../base/ReferenceModel.js";
6
- import { provideReact } from "../../lib/provideReact.js";
6
+ import { provideReact, } from "../../react/provideReact.js";
7
+ import { ListQueryModel } from "../../base/ListQueryModel.js";
8
+ import { ListDataModel } from "../../base/ListDataModel.js";
9
+ import { Project } from "../../project/index.js";
7
10
  export class AppInstallation extends ReferenceModel {
8
11
  static find = provideReact(async (id) => {
9
12
  const data = await config.behaviors.appInstallation.find(id);
@@ -19,11 +22,17 @@ export class AppInstallation extends ReferenceModel {
19
22
  static ofId(id) {
20
23
  return new AppInstallation(id);
21
24
  }
25
+ query(project, query = {}) {
26
+ return new AppInstallationListQuery(project, query);
27
+ }
28
+ /** @deprecated: use query() or project.appInstallations */
22
29
  static list = provideReact(async (projectId, query = {}) => {
23
- const data = await config.behaviors.appInstallation.list(projectId, query);
24
- return data.map((d) => new AppInstallationListItem(d));
30
+ return new AppInstallationListQuery(Project.ofId(projectId), query)
31
+ .execute()
32
+ .then((r) => r.items);
25
33
  });
26
- getDetailed = provideReact(() => AppInstallation.get(this.id));
34
+ getDetailed = provideReact(() => AppInstallation.get(this.id), [this.id]);
35
+ findDetailed = provideReact(() => AppInstallation.find(this.id), [this.id]);
27
36
  }
28
37
  // Common class for future extension
29
38
  class AppInstallationCommon extends classes((DataModel), AppInstallation) {
@@ -41,3 +50,38 @@ export class AppInstallationListItem extends classes(AppInstallationCommon, (Dat
41
50
  super([data], [data]);
42
51
  }
43
52
  }
53
+ export class AppInstallationListQuery extends ListQueryModel {
54
+ project;
55
+ constructor(project, query = {}) {
56
+ super(query);
57
+ this.project = project;
58
+ }
59
+ refine(query) {
60
+ return new AppInstallationListQuery(this.project, {
61
+ ...this.query,
62
+ ...query,
63
+ });
64
+ }
65
+ execute = provideReact(async () => {
66
+ const { items, totalCount } = await config.behaviors.appInstallation.list(this.project.id, {
67
+ limit: config.defaultPaginationLimit,
68
+ ...this.query,
69
+ });
70
+ return new AppInstallationList(this.project, this.query, items.map((d) => new AppInstallationListItem(d)), totalCount);
71
+ }, [this.queryId]);
72
+ getTotalCount = provideReact(async () => {
73
+ const { totalCount } = await this.refine({ limit: 1 }).execute();
74
+ return totalCount;
75
+ }, [this.queryId]);
76
+ findOneAndOnly = provideReact(async () => {
77
+ const { items, totalCount } = await this.refine({ limit: 2 }).execute();
78
+ if (totalCount === 1) {
79
+ return items[0];
80
+ }
81
+ }, [this.queryId]);
82
+ }
83
+ export class AppInstallationList extends classes(AppInstallationListQuery, (ListDataModel)) {
84
+ constructor(project, query, appInstallations, totalCount) {
85
+ super([project, query], [appInstallations, totalCount]);
86
+ }
87
+ }
@@ -1,4 +1,4 @@
1
- import { assertStatus } from "@mittwald/api-client";
1
+ import { assertStatus, extractTotalCountHeader, } from "@mittwald/api-client";
2
2
  import { assertOneOfStatus } from "@mittwald/api-client";
3
3
  export const apiAppInstallationBehaviors = (client) => ({
4
4
  find: async (id) => {
@@ -16,6 +16,9 @@ export const apiAppInstallationBehaviors = (client) => ({
16
16
  projectId,
17
17
  });
18
18
  assertStatus(response, 200);
19
- return response.data;
19
+ return {
20
+ items: response.data,
21
+ totalCount: extractTotalCountHeader(response),
22
+ };
20
23
  },
21
24
  });
@@ -0,0 +1,8 @@
1
+ export class ListDataModel {
2
+ items;
3
+ totalCount;
4
+ constructor(items, totalCount) {
5
+ this.items = Object.freeze(items);
6
+ this.totalCount = totalCount;
7
+ }
8
+ }
@@ -0,0 +1,9 @@
1
+ import { hash } from "object-code";
2
+ export class ListQueryModel {
3
+ query;
4
+ queryId;
5
+ constructor(query) {
6
+ this.query = query;
7
+ this.queryId = hash(query).toString();
8
+ }
9
+ }
@@ -1 +1,6 @@
1
+ export * from "./assertObjectFound.js";
2
+ export * from "./DataModel.js";
3
+ export * from "./ReferenceModel.js";
4
+ export * from "./ListDataModel.js";
5
+ export * from "./ListQueryModel.js";
1
6
  export * from "./types.js";
@@ -5,6 +5,7 @@ import { apiServerBehaviors } from "../../server/Server/behaviors/index.js";
5
5
  import { apiCustomerBehaviors } from "../../customer/Customer/behaviors/index.js";
6
6
  import { apiIngressBehaviors } from "../../domain/Ingress/behaviors/index.js";
7
7
  import { apiAppInstallationBehaviors } from "../../app/AppInstallation/behaviors/index.js";
8
+ import { addUrlTagToProvideReactCache } from "../../react/asyncResourceInvalidation.js";
8
9
  class ApiSetupState {
9
10
  _client;
10
11
  setupWithClient(client) {
@@ -12,6 +13,8 @@ class ApiSetupState {
12
13
  throw new Error("API already setup. If you want to operate on the API client use api.client.");
13
14
  }
14
15
  this._client = client;
16
+ this._client.defaultRequestOptions.onBeforeRequest =
17
+ addUrlTagToProvideReactCache;
15
18
  config.behaviors.project = apiProjectBehaviors(client);
16
19
  config.behaviors.server = apiServerBehaviors(client);
17
20
  config.behaviors.customer = apiCustomerBehaviors(client);
@@ -1,4 +1,5 @@
1
1
  export const config = {
2
+ defaultPaginationLimit: 50,
2
3
  behaviors: {
3
4
  project: undefined,
4
5
  server: undefined,
@@ -3,8 +3,23 @@ import { classes } from "polytype";
3
3
  import { DataModel } from "../../base/DataModel.js";
4
4
  import assertObjectFound from "../../base/assertObjectFound.js";
5
5
  import { ReferenceModel } from "../../base/ReferenceModel.js";
6
- import { provideReact } from "../../lib/provideReact.js";
6
+ import { provideReact, } from "../../react/provideReact.js";
7
+ import { ListQueryModel } from "../../base/ListQueryModel.js";
8
+ import { ListDataModel } from "../../base/ListDataModel.js";
9
+ import { ServerListQuery } from "../../server/index.js";
10
+ import { ProjectListQuery } from "../../project/index.js";
7
11
  export class Customer extends ReferenceModel {
12
+ servers;
13
+ projects;
14
+ constructor(id) {
15
+ super(id);
16
+ this.servers = new ServerListQuery({
17
+ customer: this,
18
+ });
19
+ this.projects = new ProjectListQuery({
20
+ customer: this,
21
+ });
22
+ }
8
23
  static ofId(id) {
9
24
  return new Customer(id);
10
25
  }
@@ -14,16 +29,21 @@ export class Customer extends ReferenceModel {
14
29
  return new CustomerDetailed(data);
15
30
  }
16
31
  });
17
- static list = provideReact(async (query = {}) => {
18
- const data = await config.behaviors.customer.list(query);
19
- return Object.freeze(data.map((d) => new CustomerListItem(d)));
20
- });
32
+ static query(query = {}) {
33
+ return new CustomerListQuery(query);
34
+ }
35
+ /** @deprecated Use query() */
36
+ static list = provideReact(async (query = {}) => new CustomerListQuery(query).execute().then((res) => res.items));
21
37
  static get = provideReact(async (id) => {
22
38
  const customer = await this.find(id);
23
39
  assertObjectFound(customer, this, id);
24
40
  return customer;
25
41
  });
26
- getDetailed = provideReact(() => Customer.get(this.id));
42
+ getDetailed = provideReact(() => Customer.get(this.id), [this.id]);
43
+ findDetailed = provideReact(() => Customer.find(this.id), [this.id]);
44
+ async update(data) {
45
+ await config.behaviors.customer.update(this.id, data);
46
+ }
27
47
  }
28
48
  // Common class for future extension
29
49
  class CustomerCommon extends classes((DataModel), Customer) {
@@ -41,3 +61,36 @@ export class CustomerListItem extends classes(CustomerCommon, (DataModel)) {
41
61
  super([data], [data]);
42
62
  }
43
63
  }
64
+ export class CustomerListQuery extends ListQueryModel {
65
+ constructor(query = {}) {
66
+ super(query);
67
+ }
68
+ refine(query) {
69
+ return new CustomerListQuery({
70
+ ...this.query,
71
+ ...query,
72
+ });
73
+ }
74
+ execute = provideReact(async () => {
75
+ const { items, totalCount } = await config.behaviors.customer.list({
76
+ limit: config.defaultPaginationLimit,
77
+ ...this.query,
78
+ });
79
+ return new CustomerList(this.query, items.map((d) => new CustomerListItem(d)), totalCount);
80
+ }, [this.queryId]);
81
+ getTotalCount = provideReact(async () => {
82
+ const { totalCount } = await this.refine({ limit: 1 }).execute();
83
+ return totalCount;
84
+ }, [this.queryId]);
85
+ findOneAndOnly = provideReact(async () => {
86
+ const { items, totalCount } = await this.refine({ limit: 2 }).execute();
87
+ if (totalCount === 1) {
88
+ return items[0];
89
+ }
90
+ }, [this.queryId]);
91
+ }
92
+ export class CustomerList extends classes(CustomerListQuery, (ListDataModel)) {
93
+ constructor(query, customers, totalCount) {
94
+ super([query], [customers, totalCount]);
95
+ }
96
+ }
@@ -1,4 +1,4 @@
1
- import { assertStatus } from "@mittwald/api-client";
1
+ import { assertStatus, extractTotalCountHeader, } from "@mittwald/api-client";
2
2
  import { assertOneOfStatus } from "@mittwald/api-client";
3
3
  export const apiCustomerBehaviors = (client) => ({
4
4
  find: async (id) => {
@@ -15,6 +15,16 @@ export const apiCustomerBehaviors = (client) => ({
15
15
  queryParameters: query,
16
16
  });
17
17
  assertStatus(response, 200);
18
- return response.data;
18
+ return {
19
+ items: response.data,
20
+ totalCount: extractTotalCountHeader(response),
21
+ };
22
+ },
23
+ update: async (id, data) => {
24
+ const response = await client.customer.updateCustomer({
25
+ customerId: id,
26
+ data,
27
+ });
28
+ assertStatus(response, 200);
19
29
  },
20
30
  });
@@ -3,8 +3,10 @@ import { classes } from "polytype";
3
3
  import { DataModel } from "../../base/DataModel.js";
4
4
  import assertObjectFound from "../../base/assertObjectFound.js";
5
5
  import { ReferenceModel } from "../../base/ReferenceModel.js";
6
- import { provideReact } from "../../lib/provideReact.js";
6
+ import { provideReact, } from "../../react/provideReact.js";
7
7
  import { IngressPath } from "../IngressPath/IngressPath.js";
8
+ import { ListQueryModel } from "../../base/ListQueryModel.js";
9
+ import { ListDataModel } from "../../base/ListDataModel.js";
8
10
  export class Ingress extends ReferenceModel {
9
11
  static ofId(id) {
10
12
  return new Ingress(id);
@@ -12,10 +14,8 @@ export class Ingress extends ReferenceModel {
12
14
  static ofHostname(hostname) {
13
15
  return Ingress.ofId(hostname);
14
16
  }
15
- static list = provideReact(async (query = {}) => {
16
- const data = await config.behaviors.ingress.list(query);
17
- return data.map((d) => new IngressListItem(d));
18
- });
17
+ /** @deprecated: use query() or project.ingresses */
18
+ static list = provideReact(async (query = {}) => new IngressListQuery(query).execute().then((r) => r.items));
19
19
  static find = provideReact(async (id) => {
20
20
  const data = await config.behaviors.ingress.find(id);
21
21
  if (data !== undefined) {
@@ -27,7 +27,8 @@ export class Ingress extends ReferenceModel {
27
27
  assertObjectFound(ingress, this, id);
28
28
  return ingress;
29
29
  });
30
- getDetailed = provideReact(() => Ingress.get(this.id));
30
+ getDetailed = provideReact(() => Ingress.get(this.id), [this.id]);
31
+ findDetailed = provideReact(() => Ingress.find(this.id), [this.id]);
31
32
  }
32
33
  export class IngressCommon extends classes((DataModel), Ingress) {
33
34
  baseUrl;
@@ -54,3 +55,52 @@ export class IngressListItem extends classes(IngressCommon, (DataModel)) {
54
55
  super([data], [data]);
55
56
  }
56
57
  }
58
+ export class IngressListQuery extends ListQueryModel {
59
+ constructor(query = {}) {
60
+ super(query);
61
+ }
62
+ refine(query = {}) {
63
+ return new IngressListQuery({
64
+ ...this.query,
65
+ ...query,
66
+ });
67
+ }
68
+ execute = provideReact(async () => {
69
+ const { project, ...query } = this.query;
70
+ const { items, totalCount } = await config.behaviors.ingress.list({
71
+ /** @todo: use this code when pagination is supported by API */
72
+ // limit: config.defaultPaginationLimit,
73
+ ...query,
74
+ projectId: project?.id,
75
+ });
76
+ return new IngressList(this.query, items.map((d) => new IngressListItem(d)), totalCount);
77
+ }, [this.queryId]);
78
+ getTotalCount = provideReact(async () => {
79
+ /** @todo: use this code when pagination is supported by API */
80
+ // const { totalCount } = await this.refine({ limit: 1 }).execute();
81
+ // return totalCount;
82
+ const { items } = await this.refine({}).execute();
83
+ return items.length;
84
+ }, [this.queryId]);
85
+ findOneAndOnly = provideReact(async () => {
86
+ /** @todo: use this code when pagination is supported by API */
87
+ // const { items, totalCount } = await this.refine({ limit: 2 }).execute();
88
+ // if (totalCount === 1) {
89
+ // return items[0];
90
+ // }
91
+ const { items, totalCount } = await this.refine({}).execute();
92
+ if (totalCount === 1) {
93
+ return items[0];
94
+ }
95
+ }, [this.queryId]);
96
+ }
97
+ export class IngressList extends classes(IngressListQuery, (ListDataModel)) {
98
+ constructor(query, ingresses, totalCount) {
99
+ super([query], [ingresses, totalCount]);
100
+ }
101
+ getDefault() {
102
+ const defaultIngress = this.items.find((i) => i.data.isDefault);
103
+ assertObjectFound(defaultIngress, IngressListItem, "IngressList");
104
+ return defaultIngress;
105
+ }
106
+ }
@@ -11,13 +11,15 @@ export const apiIngressBehaviors = (client) => ({
11
11
  assertOneOfStatus(response, [404]);
12
12
  },
13
13
  list: async (query = {}) => {
14
- const { projectId } = query;
15
14
  const response = await client.domain.ingressListIngresses({
16
- queryParameters: {
17
- projectId,
18
- },
15
+ queryParameters: query,
19
16
  });
20
17
  assertStatus(response, 200);
21
- return response.data;
18
+ return {
19
+ items: response.data,
20
+ totalCount: response.data.length,
21
+ /** @todo: use this code when pagination is supported by API */
22
+ // totalCount: extractTotalCountHeader(response),
23
+ };
22
24
  },
23
25
  });
@@ -3,11 +3,23 @@ import { classes } from "polytype";
3
3
  import { DataModel } from "../../base/DataModel.js";
4
4
  import assertObjectFound from "../../base/assertObjectFound.js";
5
5
  import { Server } from "../../server/index.js";
6
- import { provideReact, } from "../../lib/provideReact.js";
6
+ import { provideReact, } from "../../react/provideReact.js";
7
7
  import { Customer } from "../../customer/Customer/Customer.js";
8
8
  import { ReferenceModel } from "../../base/ReferenceModel.js";
9
- import { Ingress, IngressListItem } from "../../domain/index.js";
9
+ import { Ingress, IngressListItem, IngressListQuery, } from "../../domain/index.js";
10
+ import { ListQueryModel } from "../../base/ListQueryModel.js";
11
+ import { ListDataModel } from "../../base/ListDataModel.js";
12
+ import { AppInstallationListQuery } from "../../app/index.js";
10
13
  export class Project extends ReferenceModel {
14
+ ingresses;
15
+ appInstallations;
16
+ constructor(id) {
17
+ super(id);
18
+ this.ingresses = new IngressListQuery({
19
+ project: this,
20
+ });
21
+ this.appInstallations = new AppInstallationListQuery(this);
22
+ }
11
23
  static ofId(id) {
12
24
  return new Project(id);
13
25
  }
@@ -22,15 +34,20 @@ export class Project extends ReferenceModel {
22
34
  assertObjectFound(project, this, id);
23
35
  return project;
24
36
  });
37
+ static query(query = {}) {
38
+ return new ProjectListQuery(query);
39
+ }
40
+ /** @deprecated: use query(), Customer.projects or Server.projects */
25
41
  static list = provideReact(async (query = {}) => {
26
- const data = await config.behaviors.project.list(query);
27
- return Object.freeze(data.map((d) => new ProjectListItem(d)));
42
+ return new ProjectListQuery(query).execute().then((r) => r.items);
28
43
  });
29
44
  static async create(serverId, description) {
30
45
  const { id } = await config.behaviors.project.create(serverId, description);
31
46
  return new Project(id);
32
47
  }
33
- getDetailed = provideReact(() => Project.get(this.id));
48
+ getDetailed = provideReact(() => Project.get(this.id), [this.id]);
49
+ findDetailed = provideReact(() => Project.find(this.id), [this.id]);
50
+ /** @deprecated: use ingresses property */
34
51
  listIngresses = provideReact(() => Ingress.list({ projectId: this.id }));
35
52
  getDefaultIngress = provideReact(async () => {
36
53
  const ingresses = await Project.ofId(this.id).listIngresses();
@@ -67,3 +84,38 @@ export class ProjectListItem extends classes(ProjectCommon, (DataModel)) {
67
84
  super([data], [data]);
68
85
  }
69
86
  }
87
+ export class ProjectListQuery extends ListQueryModel {
88
+ constructor(query = {}) {
89
+ super(query);
90
+ }
91
+ refine(query) {
92
+ return new ProjectListQuery({
93
+ ...this.query,
94
+ ...query,
95
+ });
96
+ }
97
+ execute = provideReact(async () => {
98
+ const { server, customer, ...query } = this.query;
99
+ const { items, totalCount } = await config.behaviors.project.list({
100
+ ...query,
101
+ serverId: server?.id,
102
+ customerId: customer?.id,
103
+ });
104
+ return new ProjectList(this.query, items.map((d) => new ProjectListItem(d)), totalCount);
105
+ }, [this.queryId]);
106
+ getTotalCount = provideReact(async () => {
107
+ const { totalCount } = await this.refine({ limit: 1 }).execute();
108
+ return totalCount;
109
+ }, [this.queryId]);
110
+ findOneAndOnly = provideReact(async () => {
111
+ const { items, totalCount } = await this.refine({ limit: 2 }).execute();
112
+ if (totalCount === 1) {
113
+ return items[0];
114
+ }
115
+ }, [this.queryId]);
116
+ }
117
+ export class ProjectList extends classes(ProjectListQuery, (ListDataModel)) {
118
+ constructor(query, projects, totalCount) {
119
+ super([query], [projects, totalCount]);
120
+ }
121
+ }
@@ -1,4 +1,4 @@
1
- import { assertStatus } from "@mittwald/api-client";
1
+ import { assertStatus, extractTotalCountHeader, } from "@mittwald/api-client";
2
2
  import { assertOneOfStatus } from "@mittwald/api-client";
3
3
  export const apiProjectBehaviors = (client) => ({
4
4
  find: async (id) => {
@@ -15,7 +15,10 @@ export const apiProjectBehaviors = (client) => ({
15
15
  queryParameters: query,
16
16
  });
17
17
  assertStatus(response, 200);
18
- return response.data;
18
+ return {
19
+ items: response.data,
20
+ totalCount: extractTotalCountHeader(response),
21
+ };
19
22
  },
20
23
  create: async (serverId, description) => {
21
24
  const response = await client.project.createProject({
@@ -1,12 +1,16 @@
1
1
  export const inMemProjectBehaviors = (store) => ({
2
2
  find: async (id) => store.get(id),
3
3
  list: async () => {
4
- return Array.from(store.values()).map((detailedProject) => ({
4
+ const items = Array.from(store.values()).map((detailedProject) => ({
5
5
  ...detailedProject,
6
6
  customerMeta: {
7
7
  id: detailedProject.customerId,
8
8
  },
9
9
  }));
10
+ return {
11
+ items,
12
+ totalCount: items.length,
13
+ };
10
14
  },
11
15
  create: async () => {
12
16
  throw new Error("Not implemented");
@@ -1,12 +1,12 @@
1
- import React, { createElement } from "react";
1
+ import { usePromise } from "@mittwald/react-use-promise";
2
2
  import { setModule } from "./reactUsePromise.js";
3
- let loadingPromise = undefined;
4
3
  export const MittwaldApiModelProvider = (props) => {
5
- if (loadingPromise === undefined) {
6
- loadingPromise = import("@mittwald/react-use-promise").then((m) => {
7
- setModule(m);
8
- });
9
- throw loadingPromise;
4
+ const { fallback, children } = props;
5
+ const module = usePromise(() => import("@mittwald/react-use-promise").then(setModule), [], {
6
+ useSuspense: false,
7
+ });
8
+ if (!module.hasValue) {
9
+ return fallback;
10
10
  }
11
- return createElement(React.Fragment, undefined, props.children);
11
+ return children;
12
12
  };
@@ -0,0 +1,29 @@
1
+ import { reactProvisionContext } from "./reactProvisionContext.js";
2
+ import { refresh } from "@mittwald/react-use-promise";
3
+ import { Store } from "@mittwald/react-use-promise/store";
4
+ const cacheTagStore = new Store();
5
+ export const refreshProvideReactCache = (tag) => {
6
+ cacheTagStore.getAll(tag).forEach((ids) => {
7
+ ids.forEach((id) => {
8
+ refresh({
9
+ tag: String(id),
10
+ });
11
+ });
12
+ });
13
+ };
14
+ export const addTagToProvideReactCache = (tag) => {
15
+ const context = reactProvisionContext.use();
16
+ if (context) {
17
+ const ids = cacheTagStore.get(tag) ?? new Set();
18
+ ids.add(context.id);
19
+ cacheTagStore.set(tag, () => ids, {
20
+ tags: [tag],
21
+ });
22
+ }
23
+ };
24
+ export const addUrlTagToProvideReactCache = (request) => {
25
+ const url = request.requestConfig.url;
26
+ if (request.requestConfig.method === "GET" && url) {
27
+ addTagToProvideReactCache(url);
28
+ }
29
+ };
@@ -1,3 +1,4 @@
1
+ export { refreshProvideReactCache, addTagToProvideReactCache, addUrlTagToProvideReactCache, } from "./asyncResourceInvalidation.js";
1
2
  export * from "./MittwaldApiModelProvider.js";
2
3
  export * from "./reactUsePromise.js";
3
- export { provideReact } from "../lib/provideReact.js";
4
+ export { provideReact } from "./provideReact.js";