@synnaxlabs/client 0.26.7 → 0.28.0

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 (140) hide show
  1. package/.turbo/turbo-build.log +7 -7
  2. package/README.md +36 -10
  3. package/api/client.api.md +3121 -0
  4. package/api-extractor.json +7 -0
  5. package/dist/access/client.d.ts +0 -1
  6. package/dist/access/payload.d.ts +0 -1
  7. package/dist/auth/auth.d.ts +0 -1
  8. package/dist/channel/client.d.ts +6 -2
  9. package/dist/channel/client.d.ts.map +1 -1
  10. package/dist/channel/creator.d.ts +0 -1
  11. package/dist/channel/payload.d.ts +4 -1
  12. package/dist/channel/payload.d.ts.map +1 -1
  13. package/dist/channel/retriever.d.ts +0 -1
  14. package/dist/channel/writer.d.ts +0 -1
  15. package/dist/client.cjs +23 -19
  16. package/dist/client.d.ts +10 -7
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +2326 -1904
  19. package/dist/connection/checker.d.ts +21 -2
  20. package/dist/connection/checker.d.ts.map +1 -1
  21. package/dist/control/client.d.ts +0 -1
  22. package/dist/control/client.d.ts.map +1 -1
  23. package/dist/control/state.d.ts +0 -1
  24. package/dist/errors.d.ts +0 -1
  25. package/dist/framer/adapter.d.ts +0 -1
  26. package/dist/framer/client.d.ts +0 -1
  27. package/dist/framer/client.d.ts.map +1 -1
  28. package/dist/framer/deleter.d.ts +0 -1
  29. package/dist/framer/frame.d.ts +11 -12
  30. package/dist/framer/iterator.d.ts +0 -1
  31. package/dist/framer/streamProxy.d.ts +0 -1
  32. package/dist/framer/streamer.d.ts +0 -1
  33. package/dist/framer/writer.d.ts +0 -1
  34. package/dist/framer/writer.d.ts.map +1 -1
  35. package/dist/hardware/client.d.ts +0 -1
  36. package/dist/hardware/device/client.d.ts +0 -1
  37. package/dist/hardware/device/payload.d.ts +0 -1
  38. package/dist/hardware/device/payload.d.ts.map +1 -1
  39. package/dist/hardware/rack/client.d.ts +0 -1
  40. package/dist/hardware/rack/payload.d.ts +0 -1
  41. package/dist/hardware/rack/payload.d.ts.map +1 -1
  42. package/dist/hardware/task/client.d.ts +12 -3
  43. package/dist/hardware/task/client.d.ts.map +1 -1
  44. package/dist/hardware/task/ni/types.d.ts +14495 -0
  45. package/dist/hardware/task/ni/types.d.ts.map +1 -0
  46. package/dist/hardware/task/payload.d.ts +6 -1
  47. package/dist/hardware/task/payload.d.ts.map +1 -1
  48. package/dist/label/client.d.ts +8 -6
  49. package/dist/label/client.d.ts.map +1 -1
  50. package/dist/label/payload.d.ts +0 -1
  51. package/dist/label/retriever.d.ts +0 -1
  52. package/dist/label/retriever.d.ts.map +1 -1
  53. package/dist/label/writer.d.ts +32 -2
  54. package/dist/label/writer.d.ts.map +1 -1
  55. package/dist/ontology/client.d.ts +136 -12
  56. package/dist/ontology/client.d.ts.map +1 -1
  57. package/dist/ontology/group/client.d.ts +0 -1
  58. package/dist/ontology/group/group.d.ts +0 -1
  59. package/dist/ontology/group/payload.d.ts +0 -1
  60. package/dist/ontology/group/writer.d.ts +0 -1
  61. package/dist/ontology/group/writer.d.ts.map +1 -1
  62. package/dist/ontology/payload.d.ts +6 -3
  63. package/dist/ontology/payload.d.ts.map +1 -1
  64. package/dist/ontology/writer.d.ts +4 -5
  65. package/dist/ontology/writer.d.ts.map +1 -1
  66. package/dist/ranger/alias.d.ts +0 -1
  67. package/dist/ranger/client.d.ts +57 -14
  68. package/dist/ranger/client.d.ts.map +1 -1
  69. package/dist/ranger/external.d.ts +1 -1
  70. package/dist/ranger/external.d.ts.map +1 -1
  71. package/dist/ranger/kv.d.ts +42 -5
  72. package/dist/ranger/kv.d.ts.map +1 -1
  73. package/dist/ranger/payload.d.ts +5 -2
  74. package/dist/ranger/payload.d.ts.map +1 -1
  75. package/dist/ranger/writer.d.ts +107 -2
  76. package/dist/ranger/writer.d.ts.map +1 -1
  77. package/dist/setupspecs.d.ts +0 -1
  78. package/dist/signals/observable.d.ts +0 -1
  79. package/dist/transport.d.ts +0 -1
  80. package/dist/user/client.d.ts +0 -1
  81. package/dist/user/payload.d.ts +0 -1
  82. package/dist/util/retrieve.d.ts +0 -1
  83. package/dist/util/telem.d.ts +0 -1
  84. package/dist/util/zod.d.ts +0 -1
  85. package/dist/workspace/client.d.ts +0 -1
  86. package/dist/workspace/lineplot/client.d.ts +0 -1
  87. package/dist/workspace/lineplot/payload.d.ts +0 -1
  88. package/dist/workspace/lineplot/retriever.d.ts +0 -1
  89. package/dist/workspace/lineplot/writer.d.ts +0 -1
  90. package/dist/workspace/payload.d.ts +0 -1
  91. package/dist/workspace/retriever.d.ts +0 -1
  92. package/dist/workspace/schematic/client.d.ts +0 -1
  93. package/dist/workspace/schematic/payload.d.ts +0 -1
  94. package/dist/workspace/schematic/retriever.d.ts +0 -1
  95. package/dist/workspace/schematic/writer.d.ts +0 -1
  96. package/dist/workspace/writer.d.ts +0 -1
  97. package/package.json +14 -12
  98. package/src/access/access.spec.ts +11 -11
  99. package/src/channel/batchRetriever.spec.ts +2 -0
  100. package/src/channel/channel.spec.ts +51 -31
  101. package/src/channel/client.ts +7 -0
  102. package/src/channel/payload.ts +1 -0
  103. package/src/client.ts +17 -8
  104. package/src/connection/checker.ts +58 -1
  105. package/src/connection/connection.spec.ts +43 -3
  106. package/src/control/client.ts +9 -0
  107. package/src/errors.spec.ts +9 -0
  108. package/src/framer/client.ts +0 -1
  109. package/src/framer/frame.spec.ts +2 -2
  110. package/src/framer/frame.ts +22 -22
  111. package/src/framer/writer.ts +2 -1
  112. package/src/hardware/device/payload.ts +9 -0
  113. package/src/hardware/rack/payload.ts +9 -0
  114. package/src/hardware/task/client.ts +82 -6
  115. package/src/hardware/task/ni/types.ts +1716 -0
  116. package/src/hardware/task/payload.ts +10 -0
  117. package/src/hardware/task/task.spec.ts +45 -30
  118. package/src/label/client.ts +49 -19
  119. package/src/label/label.spec.ts +9 -0
  120. package/src/label/retriever.ts +2 -1
  121. package/src/label/writer.ts +11 -3
  122. package/src/ontology/client.ts +227 -14
  123. package/src/ontology/group/writer.ts +10 -12
  124. package/src/ontology/ontology.spec.ts +3 -5
  125. package/src/ontology/payload.ts +5 -1
  126. package/src/ontology/writer.ts +26 -12
  127. package/src/ranger/client.ts +223 -41
  128. package/src/ranger/external.ts +1 -1
  129. package/src/ranger/kv.ts +50 -11
  130. package/src/ranger/payload.ts +9 -5
  131. package/src/ranger/ranger.spec.ts +114 -49
  132. package/src/ranger/writer.ts +7 -2
  133. package/src/vite-env.d.ts +1 -1
  134. package/vite.config.ts +6 -1
  135. package/dist/ranger/active.d.ts +0 -11
  136. package/dist/ranger/active.d.ts.map +0 -1
  137. package/dist/ranger/range.d.ts +0 -32
  138. package/dist/ranger/range.d.ts.map +0 -1
  139. package/src/ranger/active.ts +0 -74
  140. package/src/ranger/range.ts +0 -98
@@ -1,3 +1,12 @@
1
+ // Copyright 2024 Synnax Labs, Inc.
2
+ //
3
+ // Use of this software is governed by the Business Source License included in the file
4
+ // licenses/BSL.txt.
5
+ //
6
+ // As of the Change Date specified in that file, in accordance with the Business Source
7
+ // License, use of this software will be governed by the Apache License, Version 2.0,
8
+ // included in the file licenses/APL.txt.
9
+
1
10
  import { binary, observe, UnknownRecord } from "@synnaxlabs/x";
2
11
  import { z } from "zod";
3
12
 
@@ -46,6 +55,7 @@ export const taskZ = z.object({
46
55
  }),
47
56
  ) as z.ZodType<UnknownRecord>,
48
57
  state: stateZ.optional().nullable(),
58
+ snapshot: z.boolean().optional(),
49
59
  });
50
60
 
51
61
  export const newTaskZ = taskZ.omit({ key: true }).extend({
@@ -44,46 +44,61 @@ describe("Hardware", () => {
44
44
  expect(retrieved.config).toStrictEqual({ a: "dog" });
45
45
  expect(retrieved.type).toBe("ni");
46
46
  });
47
- });
48
- describe("retrieveByName", () => {
49
- it("should retrieve a task by its name", async () => {
50
- const name = `test-${Date.now()}-${Math.random()}`;
51
- const r = await client.hardware.racks.create({ name });
52
- const m = await r.createTask({
53
- name,
54
- config: { a: "dog" },
55
- type: "ni",
47
+ describe("retrieveByName", () => {
48
+ it("should retrieve a task by its name", async () => {
49
+ const name = `test-${Date.now()}-${Math.random()}`;
50
+ const r = await client.hardware.racks.create({ name });
51
+ const m = await r.createTask({
52
+ name,
53
+ config: { a: "dog" },
54
+ type: "ni",
55
+ });
56
+ const retrieved = await client.hardware.tasks.retrieveByName(name);
57
+ expect(retrieved.key).toBe(m.key);
58
+ });
59
+ });
60
+ describe("retrieve with state", () => {
61
+ it("should also send the tasks state", async () => {
62
+ const r = await client.hardware.racks.create({ name: "test" });
63
+ const t = await r.createTask({
64
+ name: "test",
65
+ config: { a: "dog" },
66
+ type: "ni",
67
+ });
68
+ const w = await client.openWriter(["sy_task_state"]);
69
+ interface StateDetails {
70
+ dog: string;
71
+ }
72
+ const state: task.State<StateDetails> = {
73
+ key: id.id(),
74
+ task: t.key,
75
+ variant: "success",
76
+ };
77
+ expect(await w.write("sy_task_state", [state])).toBeTruthy();
78
+ await w.close();
79
+ const retrieved = await client.hardware.tasks.retrieve(t.key, {
80
+ includeState: true,
81
+ });
82
+ expect(retrieved.state).not.toBeNull();
83
+ expect(retrieved.state?.variant).toBe(state.variant);
56
84
  });
57
- const retrieved = await client.hardware.tasks.retrieveByName(name);
58
- expect(retrieved.key).toBe(m.key);
59
85
  });
60
86
  });
61
- describe("retrieve with state", () => {
62
- it("should also send the tasks state", async () => {
87
+
88
+ describe("copy", () => {
89
+ it("should correctly copy the task", async () => {
63
90
  const r = await client.hardware.racks.create({ name: "test" });
64
- const t = await r.createTask({
91
+ const m = await r.createTask({
65
92
  name: "test",
66
93
  config: { a: "dog" },
67
94
  type: "ni",
68
95
  });
69
- const w = await client.openWriter(["sy_task_state"]);
70
- interface StateDetails {
71
- dog: string;
72
- }
73
- const state: task.State<StateDetails> = {
74
- key: id.id(),
75
- task: t.key,
76
- variant: "success",
77
- };
78
- expect(await w.write("sy_task_state", [state])).toBeTruthy();
79
- await w.close();
80
- const retrieved = await client.hardware.tasks.retrieve(t.key, {
81
- includeState: true,
82
- });
83
- expect(retrieved.state).not.toBeNull();
84
- expect(retrieved.state?.variant).toBe(state.variant);
96
+ const copy = await client.hardware.tasks.copy(m.key, "New Name", false);
97
+ expect(copy.name).toBe("New Name");
98
+ expect(copy.config).toStrictEqual({ a: "dog" });
85
99
  });
86
100
  });
101
+
87
102
  describe("list", () => {
88
103
  it("should list all tasks", async () => {
89
104
  const t = await client.hardware.racks.create({ name: "test" });
@@ -8,13 +8,14 @@
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
10
  import { type UnaryClient } from "@synnaxlabs/freighter";
11
+ import { observe } from "@synnaxlabs/x";
11
12
  import { type AsyncTermSearcher } from "@synnaxlabs/x/search";
12
13
 
13
14
  import { type framer } from "@/framer";
14
15
  import { type Key, type Label, labelZ } from "@/label/payload";
15
16
  import { Retriever } from "@/label/retriever";
16
- import { type NewLabelPayload,Writer } from "@/label/writer";
17
- import { type ontology } from "@/ontology";
17
+ import { type NewLabelPayload, SetOptions, Writer } from "@/label/writer";
18
+ import { ontology } from "@/ontology";
18
19
  import { signals } from "@/signals";
19
20
 
20
21
  const LABEL_SET_NAME = "sy_label_set";
@@ -25,11 +26,17 @@ export class Client implements AsyncTermSearcher<string, Key, Label> {
25
26
  private readonly retriever: Retriever;
26
27
  private readonly writer: Writer;
27
28
  private readonly frameClient: framer.Client;
29
+ private readonly ontology: ontology.Client;
28
30
 
29
- constructor(client: UnaryClient, frameClient: framer.Client) {
31
+ constructor(
32
+ client: UnaryClient,
33
+ frameClient: framer.Client,
34
+ ontology: ontology.Client,
35
+ ) {
30
36
  this.writer = new Writer(client);
31
37
  this.retriever = new Retriever(client);
32
38
  this.frameClient = frameClient;
39
+ this.ontology = ontology;
33
40
  }
34
41
 
35
42
  async search(term: string): Promise<Label[]> {
@@ -46,16 +53,20 @@ export class Client implements AsyncTermSearcher<string, Key, Label> {
46
53
  return isMany ? res : res[0];
47
54
  }
48
55
 
49
- async retrieveFor(id: ontology.ID): Promise<Label[]> {
50
- return await this.retriever.retrieveFor(id);
56
+ async retrieveFor(id: ontology.CrudeID): Promise<Label[]> {
57
+ return await this.retriever.retrieveFor(new ontology.ID(id));
51
58
  }
52
59
 
53
- async label(id: ontology.ID, labels: Key[]): Promise<void> {
54
- await this.writer.set(id, labels);
60
+ async label(
61
+ id: ontology.CrudeID,
62
+ labels: Key[],
63
+ opts: SetOptions = {},
64
+ ): Promise<void> {
65
+ await this.writer.set(new ontology.ID(id), labels, opts);
55
66
  }
56
67
 
57
- async removeLabels(id: ontology.ID, labels: Key[]): Promise<void> {
58
- await this.writer.remove(id, labels);
68
+ async removeLabels(id: ontology.CrudeID, labels: Key[]): Promise<void> {
69
+ await this.writer.remove(new ontology.ID(id), labels);
59
70
  }
60
71
 
61
72
  async page(offset: number, limit: number): Promise<Label[]> {
@@ -88,17 +99,36 @@ export class Client implements AsyncTermSearcher<string, Key, Label> {
88
99
  decodeChanges,
89
100
  );
90
101
  }
102
+
103
+ async trackLabelsOf(
104
+ id: ontology.CrudeID,
105
+ ): Promise<observe.ObservableAsyncCloseable<Label[]>> {
106
+ const wrapper = new observe.Observer<Label[]>();
107
+ const initial = (await this.retrieveFor(id)).map((l) => ({
108
+ id: new ontology.ID({ key: l.key, type: "label" }),
109
+ key: l.key,
110
+ name: l.name,
111
+ data: l,
112
+ }));
113
+ const base = await this.ontology.openDependentTracker({
114
+ target: new ontology.ID(id),
115
+ dependents: initial,
116
+ relationshipType: "labeled_by",
117
+ });
118
+ base.onChange((resources: ontology.Resource[]) =>
119
+ wrapper.notify(
120
+ resources.map((r) => ({
121
+ key: r.id.key,
122
+ color: r.data?.color as string,
123
+ name: r.data?.name as string,
124
+ })),
125
+ ),
126
+ );
127
+ return wrapper;
128
+ }
91
129
  }
92
130
 
93
131
  const decodeChanges: signals.Decoder<string, Label> = (variant, data) => {
94
- if (variant === "delete")
95
- return data.toUUIDs().map((v) => ({
96
- variant,
97
- key: v,
98
- }));
99
- return data.parseJSON(labelZ).map((l) => ({
100
- variant,
101
- key: l.key,
102
- value: l,
103
- }));
132
+ if (variant === "delete") return data.toUUIDs().map((v) => ({ variant, key: v }));
133
+ return data.parseJSON(labelZ).map((l) => ({ variant, key: l.key, value: l }));
104
134
  };
@@ -47,5 +47,14 @@ describe("Label", () => {
47
47
  expect(labels).toHaveLength(1);
48
48
  expect(labels[0].key).toEqual(l2.key);
49
49
  });
50
+ it("should replace the labels on an item", async () => {
51
+ const l1 = await client.labels.create({ name: "Label One", color: "#E774D)" });
52
+ const l2 = await client.labels.create({ name: "Label Two", color: "#E774D)" });
53
+ await client.labels.label(label.ontologyID(l1.key), [l2.key]);
54
+ await client.labels.label(label.ontologyID(l1.key), [l1.key], { replace: true });
55
+ const labels = await client.labels.retrieveFor(label.ontologyID(l1.key));
56
+ expect(labels).toHaveLength(1);
57
+ expect(labels[0].key).toEqual(l1.key);
58
+ });
50
59
  });
51
60
  });
@@ -13,6 +13,7 @@ import { z } from "zod";
13
13
 
14
14
  import { keyZ, type Label, labelZ, type Params } from "@/label/payload";
15
15
  import { ontology } from "@/ontology";
16
+ import { nullableArrayZ } from "@/util/zod";
16
17
 
17
18
  const reqZ = z.object({
18
19
  keys: keyZ.array().optional(),
@@ -25,7 +26,7 @@ const reqZ = z.object({
25
26
  type Request = z.infer<typeof reqZ>;
26
27
 
27
28
  const resZ = z.object({
28
- labels: labelZ.array().optional().default([]),
29
+ labels: nullableArrayZ(labelZ),
29
30
  });
30
31
 
31
32
  export class Retriever {
@@ -33,9 +33,13 @@ const deleteReqZ = z.object({
33
33
  const setReqZ = z.object({
34
34
  id: ontology.idZ,
35
35
  labels: keyZ.array(),
36
+ replace: z.boolean().optional(),
36
37
  });
37
38
 
38
- const removeReqZ = setReqZ;
39
+ type SetReq = z.infer<typeof setReqZ>;
40
+ export type SetOptions = Pick<SetReq, "replace">;
41
+
42
+ const removeReqZ = setReqZ.omit({ replace: true });
39
43
 
40
44
  const emptyResZ = z.object({});
41
45
 
@@ -72,11 +76,15 @@ export class Writer {
72
76
  );
73
77
  }
74
78
 
75
- async set(id: ontology.ID, labels: Key[]): Promise<void> {
79
+ async set(
80
+ id: ontology.ID,
81
+ labels: Key[],
82
+ { replace }: SetOptions = {},
83
+ ): Promise<void> {
76
84
  await sendRequired<typeof setReqZ, typeof emptyResZ>(
77
85
  this.client,
78
86
  SET_ENDPOINT,
79
- { id, labels },
87
+ { id, labels, replace },
80
88
  setReqZ,
81
89
  emptyResZ,
82
90
  );
@@ -17,14 +17,17 @@ import { framer } from "@/framer";
17
17
  import { Frame } from "@/framer/frame";
18
18
  import { group } from "@/ontology/group";
19
19
  import {
20
+ CrudeID,
20
21
  ID,
21
22
  IDPayload,
22
23
  idZ,
23
24
  parseRelationship,
24
25
  RelationshipChange,
26
+ RelationshipDirection,
25
27
  type Resource,
26
28
  ResourceChange,
27
29
  resourceSchemaZ,
30
+ resourceTypeZ,
28
31
  } from "@/ontology/payload";
29
32
  import { Writer } from "@/ontology/writer";
30
33
 
@@ -39,20 +42,21 @@ const retrieveReqZ = z.object({
39
42
  term: z.string().optional(),
40
43
  limit: z.number().optional(),
41
44
  offset: z.number().optional(),
45
+ types: resourceTypeZ.array().optional(),
42
46
  });
43
47
 
44
48
  type RetrieveRequest = z.infer<typeof retrieveReqZ>;
45
49
 
46
50
  export type RetrieveOptions = Pick<
47
51
  RetrieveRequest,
48
- "includeSchema" | "excludeFieldData"
52
+ "includeSchema" | "excludeFieldData" | "types"
49
53
  >;
50
54
 
51
55
  const retrieveResZ = z.object({
52
56
  resources: resourceSchemaZ.array(),
53
57
  });
54
58
 
55
- const parseIDs = (ids: ID | ID[] | string | string[]): IDPayload[] =>
59
+ export const parseIDs = (ids: CrudeID | CrudeID[] | string | string[]): IDPayload[] =>
56
60
  toArray(ids).map((id) => new ID(id).payload);
57
61
 
58
62
  /** The core client class for executing queries against a Synnax cluster ontology */
@@ -70,16 +74,50 @@ export class Client implements AsyncTermSearcher<string, string, Resource> {
70
74
  this.framer = framer;
71
75
  }
72
76
 
77
+ /**
78
+ * Executes a fuzzy search on the ontology for resources with names/fields similar to the
79
+ * given term.
80
+ *
81
+ * @param term The search term.
82
+ * @param options Additional options for the search.
83
+ * @param options.includeSchema Whether to include the schema of the resources in the
84
+ * results.
85
+ * @param options.excludeFieldData Whether to exclude the field data of the resources in
86
+ * the results.
87
+ * @returns A list of resources that match the search term.
88
+ */
73
89
  async search(term: string, options?: RetrieveOptions): Promise<Resource[]> {
74
90
  return await this.execRetrieve({ term, ...options });
75
91
  }
76
92
 
77
- async retrieve(id: ID | string, options?: RetrieveOptions): Promise<Resource>;
78
-
79
- async retrieve(ids: ID[] | string[], options?: RetrieveOptions): Promise<Resource[]>;
93
+ /**
94
+ * Retrieves the resource in the ontology with the given ID.
95
+ * @param id The ID of the resource to retrieve.
96
+ * @param options Additional options for the retrieval.
97
+ * @param options.includeSchema Whether to include the schema of the resource in the
98
+ * results.
99
+ * @param options.excludeFieldData Whether to exclude the field data of the resource in
100
+ * the results.
101
+ * @returns The resource with the given ID.
102
+ * @throws {QueryError} If no resource is found with the given ID.
103
+ */
104
+ async retrieve(id: CrudeID, options?: RetrieveOptions): Promise<Resource>;
105
+
106
+ /**
107
+ * Retrieves the resources in the ontology with the given IDs.
108
+ * @param ids The IDs of the resources to retrieve.
109
+ * @param options Additional options for the retrieval.
110
+ * @param options.includeSchema Whether to include the schema of the resources in the
111
+ * results.
112
+ * @param options.excludeFieldData Whether to exclude the field data of the resources in
113
+ * the results.
114
+ * @returns The resources with the given IDs.
115
+ * @throws {QueryError} If no resource is found with any of the given IDs.
116
+ */
117
+ async retrieve(ids: CrudeID[], options?: RetrieveOptions): Promise<Resource[]>;
80
118
 
81
119
  async retrieve(
82
- ids: ID | ID[] | string | string[],
120
+ ids: CrudeID | CrudeID[],
83
121
  options?: RetrieveOptions,
84
122
  ): Promise<Resource | Resource[]> {
85
123
  const resources = await this.execRetrieve({ ids: parseIDs(ids), ...options });
@@ -89,6 +127,15 @@ export class Client implements AsyncTermSearcher<string, string, Resource> {
89
127
  return resources[0];
90
128
  }
91
129
 
130
+ /**
131
+ * Retrieves resources from the ontology in a paginated manner.
132
+ *
133
+ * @param offset - The offset of the page (i.e. how many resources to skip before
134
+ * returning results).
135
+ * @param limit - The maximum number of resources to return.
136
+ * @param options - Additional options for the retrieval.
137
+ * @returns A list of resources in the ontology.
138
+ */
92
139
  async page(
93
140
  offset: number,
94
141
  limit: number,
@@ -97,36 +144,88 @@ export class Client implements AsyncTermSearcher<string, string, Resource> {
97
144
  return await this.execRetrieve({ offset, limit, ...options });
98
145
  }
99
146
 
147
+ /**
148
+ * Retrieves the children of the resources with the given IDs.
149
+ * @param ids - The IDs of the resources whose children to retrieve.
150
+ * @param options - Additional options for the retrieval.
151
+ * @param options.includeSchema - Whether to include the schema of the children in the
152
+ * results.
153
+ * @param options.excludeFieldData - Whether to exclude the field data of the children in
154
+ * the results.
155
+ * @returns The children of the resources with the given IDs.
156
+ */
100
157
  async retrieveChildren(
101
- ids: ID | ID[],
158
+ ids: CrudeID | CrudeID[],
102
159
  options?: RetrieveOptions,
103
160
  ): Promise<Resource[]> {
104
161
  return await this.execRetrieve({ ids: parseIDs(ids), children: true, ...options });
105
162
  }
106
163
 
164
+ /**
165
+ * Retrieves the parents of the resources with the given IDs.
166
+ *
167
+ * @param ids the IDs of the resources whose parents to retrieve
168
+ * @param options additional options for the retrieval
169
+ * @param options.includeSchema whether to include the schema of the parents in the results
170
+ * @param options.excludeFieldData whether to exclude the field data of the parents in the results
171
+ * @returns the parents of the resources with the given IDs
172
+ */
107
173
  async retrieveParents(
108
- ids: ID | ID[],
174
+ ids: CrudeID | CrudeID[],
109
175
  options?: RetrieveOptions,
110
176
  ): Promise<Resource[]> {
111
177
  return await this.execRetrieve({ ids: parseIDs(ids), parents: true, ...options });
112
178
  }
113
179
 
114
- async addChildren(id: ID, ...children: ID[]): Promise<void> {
180
+ /**
181
+ * Adds children to a resource in the ontology.
182
+ * @param id The ID of the resource to add children to.
183
+ * @param children The IDs of the children to add.
184
+ */
185
+ async addChildren(id: CrudeID, ...children: CrudeID[]): Promise<void> {
115
186
  return await this.writer.addChildren(id, ...children);
116
187
  }
117
188
 
118
- async removeChildren(id: ID, ...children: ID[]): Promise<void> {
189
+ /**
190
+ * Removes children from a resource in the ontology.
191
+ * @param id The ID of the resource to remove children from.
192
+ * @param children The IDs of the children
193
+ * to remove.
194
+ */
195
+ async removeChildren(id: CrudeID, ...children: CrudeID[]): Promise<void> {
119
196
  return await this.writer.removeChildren(id, ...children);
120
197
  }
121
198
 
122
- async moveChildren(from: ID, to: ID, ...children: ID[]): Promise<void> {
199
+ /**
200
+ * Moves children from one resource to another in the ontology.
201
+ * @param from The ID of the resource to move children from.
202
+ * @param to The ID of the resource to move children to.
203
+ * @param children The IDs of the children to move.
204
+ */
205
+ async moveChildren(
206
+ from: CrudeID,
207
+ to: CrudeID,
208
+ ...children: CrudeID[]
209
+ ): Promise<void> {
123
210
  return await this.writer.moveChildren(from, to, ...children);
124
211
  }
125
212
 
213
+ /**
214
+ * Opens an observable that can be used to subscribe to changes in both the ontology's
215
+ * resources and relationships.
216
+ * @see ChangeTracker for more information.
217
+ * @returns An observable that emits changes to the ontology's resources and relationships.
218
+ */
126
219
  async openChangeTracker(): Promise<ChangeTracker> {
127
220
  return await ChangeTracker.open(this.framer, this);
128
221
  }
129
222
 
223
+ async openDependentTracker(
224
+ props: DependentTrackerProps,
225
+ ): Promise<observe.ObservableAsyncCloseable<Resource[]>> {
226
+ return await DependentTracker.open(props, this.framer, this);
227
+ }
228
+
130
229
  newSearcherWithOptions(
131
230
  options: RetrieveOptions,
132
231
  ): AsyncTermSearcher<string, string, Resource> {
@@ -155,13 +254,21 @@ const RESOURCE_DELETE_NAME = "sy_ontology_resource_delete";
155
254
  const RELATIONSHIP_SET_NAME = "sy_ontology_relationship_set";
156
255
  const RELATIONSHIP_DELETE_NAME = "sy_ontology_relationship_delete";
157
256
 
257
+ /**
258
+ * A class that tracks changes to the ontology's resources and relationships.
259
+ */
158
260
  export class ChangeTracker {
159
- private readonly resourceObs: observe.Observer<ResourceChange[]>;
160
- private readonly relationshipObs: observe.Observer<RelationshipChange[]>;
161
-
261
+ /**
262
+ * An observable that emits changes to the ontology's relationships.
263
+ */
162
264
  readonly relationships: observe.Observable<RelationshipChange[]>;
265
+ /**
266
+ * An observable that emits changes to the ontology's resources.
267
+ */
163
268
  readonly resources: observe.Observable<ResourceChange[]>;
164
269
 
270
+ private readonly resourceObs: observe.Observer<ResourceChange[]>;
271
+ private readonly relationshipObs: observe.Observer<RelationshipChange[]>;
165
272
  private readonly streamer: framer.Streamer;
166
273
  private readonly client: Client;
167
274
  private readonly closePromise: Promise<void>;
@@ -256,3 +363,109 @@ export class ChangeTracker {
256
363
  return new ChangeTracker(streamer, retriever);
257
364
  }
258
365
  }
366
+
367
+ const oppositeDirection = (dir: RelationshipDirection): RelationshipDirection =>
368
+ dir === "from" ? "to" : "from";
369
+
370
+ interface DependentTrackerProps {
371
+ target: ID;
372
+ dependents: Resource[];
373
+ relationshipType?: string;
374
+ relationshipDirection?: RelationshipDirection;
375
+ resourceType?: string;
376
+ }
377
+
378
+ /**
379
+ * A class that tracks a resource (called the 'target' resource) and related resources
380
+ * (called 'dependents') of a particular type (called the 'type') in a Synnax cluster
381
+ * ontology.
382
+ */
383
+ export class DependentTracker
384
+ extends observe.Observer<Resource[]>
385
+ implements observe.ObservableAsyncCloseable<Resource[]>
386
+ {
387
+ private readonly internal: ChangeTracker;
388
+ private readonly target: ID;
389
+ private readonly relDir: RelationshipDirection;
390
+ private readonly resourceType?: string;
391
+ private dependents: Resource[];
392
+ private readonly client: Client;
393
+ private readonly relType: string;
394
+
395
+ private constructor(
396
+ {
397
+ target,
398
+ dependents,
399
+ relationshipType = "parent",
400
+ relationshipDirection = "from",
401
+ resourceType,
402
+ }: DependentTrackerProps,
403
+ internal: ChangeTracker,
404
+ client: Client,
405
+ ) {
406
+ super();
407
+ this.resourceType = resourceType;
408
+ this.internal = internal;
409
+ this.target = target;
410
+ this.dependents = dependents;
411
+ if (this.resourceType != null)
412
+ this.dependents = this.dependents.filter((r) => r.id.type === this.resourceType);
413
+ this.client = client;
414
+ this.relType = relationshipType;
415
+ this.relDir = relationshipDirection;
416
+ this.internal.resources.onChange(this.handleResourceChange);
417
+ this.internal.relationships.onChange(this.handleRelationshipChange);
418
+ }
419
+ static async open(
420
+ props: DependentTrackerProps,
421
+ framer: framer.Client,
422
+ client: Client,
423
+ ): Promise<DependentTracker> {
424
+ const internal = await ChangeTracker.open(framer, client);
425
+ return new DependentTracker(props, internal, client);
426
+ }
427
+
428
+ private handleResourceChange = (changes: ResourceChange[]): void => {
429
+ this.dependents = this.dependents.map((child) => {
430
+ const change = changes.find((c) => c.key.toString() == child.id.toString());
431
+ if (change == null || change.variant === "delete") return child;
432
+ return change.value;
433
+ });
434
+ this.notify(this.dependents);
435
+ };
436
+
437
+ private handleRelationshipChange = (changes: RelationshipChange[]): void => {
438
+ const deletes = changes.filter(
439
+ (c) =>
440
+ c.variant === "delete" &&
441
+ c.key[this.relDir].toString() === this.target.toString() &&
442
+ (this.resourceType == null ||
443
+ c.key[oppositeDirection(this.relDir)].type === this.resourceType),
444
+ );
445
+ this.dependents = this.dependents.filter(
446
+ (child) =>
447
+ !deletes.some(
448
+ (del) =>
449
+ del.key.to.toString() === child.id.toString() &&
450
+ del.key.type === this.relType,
451
+ ),
452
+ );
453
+ const sets = changes.filter(
454
+ (c) =>
455
+ c.variant === "set" &&
456
+ c.key.type === this.relType &&
457
+ c.key[this.relDir].toString() === this.target.toString() &&
458
+ (this.resourceType == null ||
459
+ c.key[oppositeDirection(this.relDir)].type === this.resourceType),
460
+ );
461
+ if (sets.length === 0) return this.notify(this.dependents);
462
+ this.client.retrieve(sets.map((s) => s.key.to)).then((resources) => {
463
+ this.dependents = this.dependents.concat(resources);
464
+ this.notify(this.dependents);
465
+ });
466
+ };
467
+
468
+ async close(): Promise<void> {
469
+ await this.internal.close();
470
+ }
471
+ }
@@ -7,10 +7,10 @@
7
7
  // License, use of this software will be governed by the Apache License, Version 2.0,
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
- import { type UnaryClient } from "@synnaxlabs/freighter";
10
+ import { sendRequired, type UnaryClient } from "@synnaxlabs/freighter";
11
11
  import { z } from "zod";
12
12
 
13
- import { groupZ,type Payload } from "@/ontology/group/payload";
13
+ import { groupZ, type Payload } from "@/ontology/group/payload";
14
14
  import { type ID, idZ } from "@/ontology/payload";
15
15
 
16
16
  const resZ = z.object({
@@ -43,35 +43,33 @@ export class Writer {
43
43
  }
44
44
 
45
45
  async create(parent: ID, name: string, key?: string): Promise<Payload> {
46
- const [res, err] = await this.client.send(
46
+ const res = await sendRequired(
47
+ this.client,
47
48
  Writer.ENDPOINT,
48
49
  { parent, name, key },
49
50
  createReqZ,
50
51
  resZ,
51
52
  );
52
- if (err != null) throw err;
53
53
  return res.group;
54
54
  }
55
55
 
56
56
  async rename(key: string, name: string): Promise<void> {
57
- const req = { key, name };
58
- const [, err] = await this.client.send(
57
+ await sendRequired(
58
+ this.client,
59
59
  Writer.ENDPOINT_RENAME,
60
- req,
60
+ { key, name },
61
61
  renameReqZ,
62
62
  z.object({}),
63
63
  );
64
- if (err != null) throw err;
65
64
  }
66
65
 
67
66
  async delete(keys: string[]): Promise<void> {
68
- const req = { keys };
69
- const [, err] = await this.client.send(
67
+ await sendRequired(
68
+ this.client,
70
69
  Writer.ENDPOINT_DELETE,
71
- req,
70
+ { keys },
72
71
  deleteReqZ,
73
72
  z.object({}),
74
73
  );
75
- if (err != null) throw err;
76
74
  }
77
75
  }