@synnaxlabs/client 0.13.5 → 0.13.6

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@synnaxlabs/client",
3
3
  "private": false,
4
- "version": "0.13.5",
4
+ "version": "0.13.6",
5
5
  "type": "module",
6
6
  "description": "The Client Library for Synnax",
7
7
  "repository": "https://github.com/synnaxlabs/synnax/tree/main/client/ts",
@@ -17,15 +17,15 @@
17
17
  ],
18
18
  "dependencies": {
19
19
  "zod": "3.22.4",
20
- "@synnaxlabs/x": "0.7.0",
21
- "@synnaxlabs/freighter": "0.4.0"
20
+ "@synnaxlabs/freighter": "0.4.1",
21
+ "@synnaxlabs/x": "0.8.0"
22
22
  },
23
23
  "devDependencies": {
24
- "@types/node": "^20.8.7",
25
- "@vitest/coverage-v8": "^0.34.6",
26
- "typescript": "^5.2.2",
27
- "vite": "^4.5.0",
28
- "vitest": "^0.34.6",
24
+ "@types/node": "^20.10.5",
25
+ "@vitest/coverage-v8": "^1.1.0",
26
+ "typescript": "^5.3.3",
27
+ "vite": "^5.0.10",
28
+ "vitest": "^1.1.0",
29
29
  "@synnaxlabs/vite-plugin": "0.0.1",
30
30
  "@synnaxlabs/tsconfig": "0.0.1",
31
31
  "eslint-config-synnaxlabs": "0.0.0"
package/src/client.ts CHANGED
@@ -15,6 +15,7 @@ import { channel } from "@/channel";
15
15
  import { connection } from "@/connection";
16
16
  import { errorsMiddleware } from "@/errors";
17
17
  import { framer } from "@/framer";
18
+ import { label } from "@/label";
18
19
  import { ontology } from "@/ontology";
19
20
  import { ranger } from "@/ranger";
20
21
  import { Transport } from "@/transport";
@@ -52,6 +53,7 @@ export default class Synnax {
52
53
  readonly ontology: ontology.Client;
53
54
  readonly props: ParsedSynnaxProps;
54
55
  readonly workspaces: workspace.Client;
56
+ readonly labels: label.Client;
55
57
  static readonly connectivity = connection.Checker;
56
58
 
57
59
  /**
@@ -96,12 +98,14 @@ export default class Synnax {
96
98
  this.ontology = new ontology.Client(this.transport.unary, this.telem);
97
99
  const rangeRetriever = new ranger.Retriever(this.transport.unary);
98
100
  const rangeWriter = new ranger.Writer(this.transport.unary);
101
+ this.labels = new label.Client(this.transport.unary, this.telem);
99
102
  this.ranges = new ranger.Client(
100
103
  this.telem,
101
104
  rangeRetriever,
102
105
  rangeWriter,
103
106
  this.transport.unary,
104
107
  chRetriever,
108
+ this.labels,
105
109
  );
106
110
  this.workspaces = new workspace.Client(this.transport.unary);
107
111
  }
@@ -21,5 +21,6 @@ export class Authority extends Number {
21
21
  .min(0)
22
22
  .max(255)
23
23
  .transform((n) => new Authority(n)),
24
+ z.instanceof(Number).transform((n) => new Authority(n)),
24
25
  ]);
25
26
  }
@@ -11,7 +11,7 @@ import { type Destructor, binary, observe } from "@synnaxlabs/x";
11
11
  import { z } from "zod";
12
12
 
13
13
  import { type Key as ChannelKey } from "@/channel/payload";
14
- import { type Authority } from "@/control/authority";
14
+ import { Authority } from "@/control/authority";
15
15
  import { type Client as FrameClient } from "@/framer/client";
16
16
  import { type Streamer as FrameStreamer } from "@/framer/streamer";
17
17
 
@@ -25,12 +25,28 @@ export interface Subject {
25
25
  key: string;
26
26
  }
27
27
 
28
+ export const stateZ = z.object({
29
+ subject: subjectZ,
30
+ resource: z.number(),
31
+ authority: Authority.z,
32
+ });
33
+
28
34
  export interface State {
29
35
  subject: Subject;
30
36
  resource: ChannelKey;
31
37
  authority: Authority;
32
38
  }
33
39
 
40
+ export const filterTransfersByChannelKey =
41
+ (...resources: ChannelKey[]) =>
42
+ (transfers: Transfer[]): Transfer[] =>
43
+ transfers.filter((t) => {
44
+ let ok = false;
45
+ if (t.to != null) ok = resources.includes(t.to.resource);
46
+ if (t.from != null && !ok) ok = resources.includes(t.from.resource);
47
+ return ok;
48
+ });
49
+
34
50
  interface Release {
35
51
  from: State;
36
52
  to?: null;
package/src/errors.ts CHANGED
@@ -78,7 +78,17 @@ export class AuthError extends BaseError {}
78
78
  /**
79
79
  * UnexpectedError is raised when an unexpected error occurs.
80
80
  */
81
- export class UnexpectedError extends BaseError {}
81
+ export class UnexpectedError extends BaseError {
82
+ constructor(message: string) {
83
+ super(`
84
+ Unexpected error encountered:
85
+
86
+ ${message}
87
+
88
+ Please report this to the Synnax team.
89
+ `);
90
+ }
91
+ }
82
92
 
83
93
  /**
84
94
  * QueryError is raised when a query error occurs.
@@ -113,7 +123,7 @@ const decode = (payload: ErrorPayload): Error | null => {
113
123
  return new AuthError(payload.data);
114
124
  case APIErrorType.Unexpected:
115
125
  return new UnexpectedError(payload.data);
116
- case APIErrorType.Validation:
126
+ case APIErrorType.Validation:
117
127
  return new ValidationError(payload.data);
118
128
  case APIErrorType.Query:
119
129
  return new QueryError(payload.data);
package/src/index.ts CHANGED
@@ -50,3 +50,4 @@ export type {
50
50
  } from "@synnaxlabs/x";
51
51
  export { workspace } from "@/workspace";
52
52
  export { ranger } from "@/ranger";
53
+ export { label } from "@/label";
@@ -0,0 +1,103 @@
1
+ // Copyright 2023 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
+
10
+ import { type UnaryClient } from "@synnaxlabs/freighter";
11
+ import { type AsyncTermSearcher } from "@synnaxlabs/x";
12
+
13
+ import { type framer } from "@/framer";
14
+ import { type Key, type Label, labelZ } from "@/label/payload";
15
+ import { Retriever } from "@/label/retriever";
16
+ import { Writer, type NewLabelPayload } from "@/label/writer";
17
+ import { type ontology } from "@/ontology";
18
+ import { signals } from "@/signals";
19
+
20
+ const LABEL_SET_NAME = "sy_label_set";
21
+ const LABEL_DELETE_NAME = "sy_label_delete";
22
+
23
+ export class Client implements AsyncTermSearcher<string, Key, Label> {
24
+ private readonly retriever: Retriever;
25
+ private readonly writer: Writer;
26
+ private readonly frameClient: framer.Client;
27
+
28
+ constructor(client: UnaryClient, frameClient: framer.Client) {
29
+ this.writer = new Writer(client);
30
+ this.retriever = new Retriever(client);
31
+ this.frameClient = frameClient;
32
+ }
33
+
34
+ async search(term: string): Promise<Label[]> {
35
+ return await this.retriever.search(term);
36
+ }
37
+
38
+ async retrieve(key: Key): Promise<Label>;
39
+
40
+ async retrieve(keys: Key[]): Promise<Label[]>;
41
+
42
+ async retrieve(keys: Key | Key[]): Promise<Label | Label[]> {
43
+ const isMany = Array.isArray(keys);
44
+ const res = await this.retriever.retrieve(keys);
45
+ return isMany ? res : res[0];
46
+ }
47
+
48
+ async retrieveFor(id: ontology.ID): Promise<Label[]> {
49
+ return await this.retriever.retrieveFor(id);
50
+ }
51
+
52
+ async label(id: ontology.ID, labels: Key[]): Promise<void> {
53
+ await this.writer.set(id, labels);
54
+ }
55
+
56
+ async removeLabels(id: ontology.ID, labels: Key[]): Promise<void> {
57
+ await this.writer.remove(id, labels);
58
+ }
59
+
60
+ async page(offset: number, limit: number): Promise<Label[]> {
61
+ return await this.retriever.page(offset, limit);
62
+ }
63
+
64
+ async create(label: NewLabelPayload): Promise<Label>;
65
+
66
+ async create(labels: NewLabelPayload[]): Promise<Label[]>;
67
+
68
+ async create(labels: NewLabelPayload | NewLabelPayload[]): Promise<Label | Label[]> {
69
+ const isMany = Array.isArray(labels);
70
+ const res = await this.writer.create(labels);
71
+ return isMany ? res : res[0];
72
+ }
73
+
74
+ async delete(key: Key): Promise<void>;
75
+
76
+ async delete(keys: Key[]): Promise<void>;
77
+
78
+ async delete(keys: Key | Key[]): Promise<void> {
79
+ await this.writer.delete(keys);
80
+ }
81
+
82
+ async openChangeTracker(): Promise<signals.Observable<string, Label>> {
83
+ return await signals.Observable.open<string, Label>(
84
+ this.frameClient,
85
+ LABEL_SET_NAME,
86
+ LABEL_DELETE_NAME,
87
+ decodeChanges,
88
+ );
89
+ }
90
+ }
91
+
92
+ const decodeChanges: signals.Decoder<string, Label> = (variant, data) => {
93
+ if (variant === "delete")
94
+ return data.toUUIDs().map((v) => ({
95
+ variant,
96
+ key: v,
97
+ }));
98
+ return data.parseJSON(labelZ).map((l) => ({
99
+ variant,
100
+ key: l.key,
101
+ value: l,
102
+ }));
103
+ };
@@ -0,0 +1,13 @@
1
+ // Copyright 2023 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
+
10
+ export * from "@/label/client";
11
+ export * from "@/label/writer";
12
+ export * from "@/label/retriever";
13
+ export * from "@/label/payload";
@@ -7,4 +7,4 @@
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
- export * as cdc from "@/cdc/external";
10
+ export * as label from "@/label/external";
@@ -0,0 +1,51 @@
1
+ // Copyright 2023 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
+
10
+ import { describe, expect, it } from "vitest";
11
+
12
+ import { label } from "@/label";
13
+ import { newClient } from "@/setupspecs";
14
+
15
+ const client = newClient();
16
+
17
+ describe("Label", () => {
18
+ describe("create", () => {
19
+ it("should create a label", async () => {
20
+ const v = await client.labels.create({ name: "Label", color: "#E774D0" });
21
+ expect(v.key).not.toHaveLength(0);
22
+ });
23
+ });
24
+
25
+ describe("retrieve", () => {
26
+ it("should retrieve a label by its key", async () => {
27
+ const v = await client.labels.create({ name: "Label", color: "#E774D0" });
28
+ const retrieved = await client.labels.retrieve(v.key);
29
+ expect(retrieved).toEqual(v);
30
+ });
31
+ });
32
+
33
+ describe("delete", () => {
34
+ it("should delete a label by its key", async () => {
35
+ const v = await client.labels.create({ name: "Label", color: "#E774D0" });
36
+ await client.labels.delete(v.key);
37
+ await expect(async () => await client.labels.retrieve(v.key)).rejects.toThrow();
38
+ });
39
+ });
40
+
41
+ describe("label", () => {
42
+ it("should set a label on an item", async () => {
43
+ const l1 = await client.labels.create({ name: "Label One", color: "#E774D)" });
44
+ const l2 = await client.labels.create({ name: "Label Two", color: "#E774D)" });
45
+ await client.labels.label(label.ontologyID(l1.key), [l2.key]);
46
+ const labels = await client.labels.retrieveFor(label.ontologyID(l1.key));
47
+ expect(labels).toHaveLength(1);
48
+ expect(labels[0].key).toEqual(l2.key);
49
+ });
50
+ });
51
+ });
@@ -0,0 +1,29 @@
1
+ // Copyright 2023 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
+
10
+ import { z } from "zod";
11
+
12
+ import { ontology } from "@/ontology";
13
+
14
+ export const keyZ = z.string().uuid();
15
+
16
+ export type Key = z.infer<typeof keyZ>;
17
+
18
+ export type Params = Key | Key[];
19
+
20
+ export const labelZ = z.object({
21
+ key: keyZ,
22
+ name: z.string().min(1),
23
+ color: z.string(),
24
+ });
25
+
26
+ export type Label = z.infer<typeof labelZ>;
27
+
28
+ export const ontologyID = (key: Key): ontology.ID =>
29
+ new ontology.ID({ type: "label", key });
@@ -0,0 +1,65 @@
1
+ // Copyright 2023 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
+
10
+ import { type UnaryClient } from "@synnaxlabs/freighter";
11
+ import { toArray } from "@synnaxlabs/x";
12
+ import { z } from "zod";
13
+
14
+ import { keyZ, type Label, labelZ, type Params } from "@/label/payload";
15
+ import { ontology } from "@/ontology";
16
+
17
+ const reqZ = z.object({
18
+ keys: keyZ.array().optional(),
19
+ for: ontology.idZ.optional(),
20
+ search: z.string().optional(),
21
+ offset: z.number().optional(),
22
+ limit: z.number().optional(),
23
+ });
24
+
25
+ type Request = z.infer<typeof reqZ>;
26
+
27
+ const resZ = z.object({
28
+ labels: labelZ.array().optional().default([]),
29
+ });
30
+
31
+ export class Retriever {
32
+ private static readonly ENDPOINT = "/label/retrieve";
33
+ private readonly client: UnaryClient;
34
+
35
+ constructor(client: UnaryClient) {
36
+ this.client = client;
37
+ }
38
+
39
+ async retrieve(params: Params): Promise<Label[]> {
40
+ const normalized = toArray(params);
41
+ return await this.execute({ keys: normalized });
42
+ }
43
+
44
+ async retrieveFor(id: ontology.ID): Promise<Label[]> {
45
+ return await this.execute({ for: id });
46
+ }
47
+
48
+ async search(term: string): Promise<Label[]> {
49
+ return await this.execute({ search: term });
50
+ }
51
+
52
+ async page(offset: number, limit: number): Promise<Label[]> {
53
+ return await this.execute({ offset, limit });
54
+ }
55
+
56
+ private async execute(req: Request): Promise<Label[]> {
57
+ const [res, err] = await this.client.send<typeof reqZ, typeof resZ>(
58
+ Retriever.ENDPOINT,
59
+ req,
60
+ resZ,
61
+ );
62
+ if (err != null) throw err;
63
+ return res.labels;
64
+ }
65
+ }
@@ -0,0 +1,90 @@
1
+ // Copyright 2023 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
+
10
+ import { sendRequired, type UnaryClient } from "@synnaxlabs/freighter";
11
+ import { toArray } from "@synnaxlabs/x";
12
+ import { z } from "zod";
13
+
14
+ import { type Key, keyZ, type Label, labelZ } from "@/label/payload";
15
+ import { ontology } from "@/ontology";
16
+
17
+ export const newLabelPayloadZ = labelZ.extend({ key: keyZ.optional() });
18
+
19
+ export type NewLabelPayload = z.infer<typeof newLabelPayloadZ>;
20
+
21
+ const createReqZ = z.object({
22
+ labels: newLabelPayloadZ.array(),
23
+ });
24
+
25
+ const createResZ = z.object({
26
+ labels: labelZ.array(),
27
+ });
28
+
29
+ const deleteReqZ = z.object({
30
+ keys: keyZ.array(),
31
+ });
32
+
33
+ const setReqZ = z.object({
34
+ id: ontology.idZ,
35
+ labels: keyZ.array(),
36
+ });
37
+
38
+ const removeReqZ = setReqZ;
39
+
40
+ const emptyResZ = z.object({});
41
+
42
+ const CREATE_ENDPOINT = "/label/create";
43
+ const DELETE_ENDPOINT = "/label/delete";
44
+ const SET_ENDPOINT = "/label/set";
45
+ const REMOVE_ENDPOINT = "/label/remove";
46
+
47
+ export class Writer {
48
+ private readonly client: UnaryClient;
49
+
50
+ constructor(client: UnaryClient) {
51
+ this.client = client;
52
+ }
53
+
54
+ async create(labels: NewLabelPayload | NewLabelPayload[]): Promise<Label[]> {
55
+ const res = await sendRequired<typeof createReqZ, typeof createResZ>(
56
+ this.client,
57
+ CREATE_ENDPOINT,
58
+ { labels: toArray(labels) },
59
+ createResZ,
60
+ );
61
+ return res.labels;
62
+ }
63
+
64
+ async delete(keys: Key | Key[]): Promise<void> {
65
+ await sendRequired<typeof deleteReqZ, typeof emptyResZ>(
66
+ this.client,
67
+ DELETE_ENDPOINT,
68
+ { keys: toArray(keys) },
69
+ emptyResZ,
70
+ );
71
+ }
72
+
73
+ async set(id: ontology.ID, labels: Key[]): Promise<void> {
74
+ await sendRequired<typeof setReqZ, typeof emptyResZ>(
75
+ this.client,
76
+ SET_ENDPOINT,
77
+ { id, labels },
78
+ emptyResZ,
79
+ );
80
+ }
81
+
82
+ async remove(id: ontology.ID, labels: Key[]): Promise<void> {
83
+ await sendRequired<typeof removeReqZ, typeof emptyResZ>(
84
+ this.client,
85
+ CREATE_ENDPOINT,
86
+ { id, labels },
87
+ emptyResZ,
88
+ );
89
+ }
90
+ }
@@ -11,7 +11,6 @@ import { type UnaryClient } from "@synnaxlabs/freighter";
11
11
  import { type AsyncTermSearcher } from "@synnaxlabs/x";
12
12
 
13
13
  import { type Client as FrameClient } from "@/framer/client";
14
- import { ChangeTracker } from "@/ontology/cdc";
15
14
  import { group } from "@/ontology/group";
16
15
  import { type ID, type Resource } from "@/ontology/payload";
17
16
  import { Retriever } from "@/ontology/retriever";
@@ -19,6 +18,8 @@ import { Writer } from "@/ontology/writer";
19
18
 
20
19
  import { QueryError } from "..";
21
20
 
21
+ import { ChangeTracker } from "@/ontology/signals";
22
+
22
23
  /** The core client class for executing queries against a Synnax cluster ontology */
23
24
  export class Client implements AsyncTermSearcher<string, string, Resource> {
24
25
  groups: group.Client;
@@ -9,4 +9,4 @@
9
9
 
10
10
  export * from "@/ontology/client";
11
11
  export * from "@/ontology/payload";
12
- export * from "@/ontology/cdc";
12
+ export * from "@/ontology/signals";
@@ -77,7 +77,7 @@ describe("Ontology", () => {
77
77
  expect(newRootLength).toEqual(oldRootLength - 1);
78
78
  });
79
79
  });
80
- describe("cdc", async () => {
80
+ describe("signals", async () => {
81
81
  it("should correctly decode a set of relationships from a string", () => {
82
82
  const rel = ontology.parseRelationship("typeA:keyA->parent->typeB:keyB");
83
83
  expect(rel.type).toEqual("parent");
@@ -10,6 +10,7 @@
10
10
  import { z } from "zod";
11
11
 
12
12
  const resourceTypeZ = z.union([
13
+ z.literal("label"),
13
14
  z.literal("builtin"),
14
15
  z.literal("cluster"),
15
16
  z.literal("channel"),
@@ -113,9 +114,5 @@ export type Relationship = z.infer<typeof relationshipSchemaZ>;
113
114
 
114
115
  export const parseRelationship = (str: string): Relationship => {
115
116
  const [from, type, to] = str.split("->");
116
- return {
117
- from: new ID(from),
118
- type,
119
- to: new ID(to),
120
- };
117
+ return { from: new ID(from), type, to: new ID(to) };
121
118
  };
@@ -11,11 +11,11 @@ import { sendRequired, type UnaryClient } from "@synnaxlabs/freighter";
11
11
  import { type change } from "@synnaxlabs/x";
12
12
  import { z } from "zod";
13
13
 
14
- import { cdc } from "@/cdc";
15
14
  import { type channel } from "@/channel";
16
15
  import { keyZ as channelKeyZ, type Key as ChannelKey } from "@/channel/payload";
17
16
  import { type Client as FrameClient } from "@/framer/client";
18
17
  import { type Key, keyZ } from "@/ranger/payload";
18
+ import { signals } from "@/signals";
19
19
 
20
20
  export const ALIAS_SET_NAME = "sy_range_alias_set";
21
21
  export const ALIAS_DELETE_NAME = "sy_range_alias_delete";
@@ -136,8 +136,8 @@ export class Aliaser {
136
136
  );
137
137
  }
138
138
 
139
- async openChangeTracker(): Promise<cdc.Observable<string, Alias>> {
140
- return await cdc.Observable.open<string, Alias>(
139
+ async openChangeTracker(): Promise<signals.Observable<string, Alias>> {
140
+ return await signals.Observable.open<string, Alias>(
141
141
  this.frameClient,
142
142
  ALIAS_SET_NAME,
143
143
  ALIAS_DELETE_NAME,
@@ -163,7 +163,7 @@ const aliasZ = z.object({
163
163
  const aliasSeparator = "---";
164
164
 
165
165
  const decodeAliasChanges =
166
- (rangeKey: Key): cdc.Decoder<string, Alias> =>
166
+ (rangeKey: Key): signals.Decoder<string, Alias> =>
167
167
  (variant, data) => {
168
168
  if (variant === "delete") {
169
169
  return data
@@ -13,6 +13,9 @@ import { type AsyncTermSearcher, toArray } from "@synnaxlabs/x";
13
13
  import { type Retriever as ChannelRetriever } from "@/channel/retriever";
14
14
  import { QueryError } from "@/errors";
15
15
  import { type framer } from "@/framer";
16
+ import { type label } from "@/label";
17
+ import { type ontology } from "@/ontology";
18
+ import { Active } from "@/ranger/active";
16
19
  import { Aliaser } from "@/ranger/alias";
17
20
  import { KV } from "@/ranger/kv";
18
21
  import {
@@ -29,8 +32,6 @@ import { Range } from "@/ranger/range";
29
32
  import { type Retriever } from "@/ranger/retriever";
30
33
  import { type Writer } from "@/ranger/writer";
31
34
 
32
- import { Active } from "./active";
33
-
34
35
  export class Client implements AsyncTermSearcher<string, Key, Range> {
35
36
  private readonly frameClient: framer.Client;
36
37
  private readonly retriever: Retriever;
@@ -38,6 +39,7 @@ export class Client implements AsyncTermSearcher<string, Key, Range> {
38
39
  private readonly unaryClient: UnaryClient;
39
40
  private readonly channels: ChannelRetriever;
40
41
  private readonly active: Active;
42
+ private readonly labelClient: label.Client;
41
43
 
42
44
  constructor(
43
45
  frameClient: framer.Client,
@@ -45,6 +47,7 @@ export class Client implements AsyncTermSearcher<string, Key, Range> {
45
47
  writer: Writer,
46
48
  unary: UnaryClient,
47
49
  channels: ChannelRetriever,
50
+ labelClient: label.Client,
48
51
  ) {
49
52
  this.frameClient = frameClient;
50
53
  this.retriever = retriever;
@@ -52,6 +55,7 @@ export class Client implements AsyncTermSearcher<string, Key, Range> {
52
55
  this.unaryClient = unary;
53
56
  this.channels = channels;
54
57
  this.active = new Active(unary);
58
+ this.labelClient = labelClient;
55
59
  }
56
60
 
57
61
  async create(range: NewPayload): Promise<Range>;
@@ -118,6 +122,7 @@ export class Client implements AsyncTermSearcher<string, Key, Range> {
118
122
  new KV(payload.key, this.unaryClient),
119
123
  new Aliaser(payload.key, this.frameClient, this.unaryClient),
120
124
  this.channels,
125
+ this.labelClient,
121
126
  );
122
127
  });
123
128
  }
@@ -19,7 +19,7 @@ export type Params = Key | Name | Keys | Names;
19
19
 
20
20
  export const payloadZ = z.object({
21
21
  key: keyZ,
22
- name: z.string(),
22
+ name: z.string().min(1),
23
23
  timeRange: TimeRange.z,
24
24
  });
25
25
  export type Payload = z.infer<typeof payloadZ>;
@@ -9,13 +9,19 @@
9
9
 
10
10
  import { type Series, TimeRange } from "@synnaxlabs/x";
11
11
 
12
- import { type cdc } from "@/cdc";
13
12
  import { type Key, type Params, type Name } from "@/channel/payload";
14
13
  import { type Retriever as ChannelRetriever } from "@/channel/retriever";
15
14
  import { QueryError } from "@/errors";
16
15
  import { type framer } from "@/framer";
16
+ import { type label } from "@/label";
17
+ import { type Label } from "@/label/payload";
18
+ import { ontology } from "@/ontology";
17
19
  import { type Alias, type Aliaser } from "@/ranger/alias";
18
20
  import { type KV } from "@/ranger/kv";
21
+ import { type signals } from "@/signals";
22
+
23
+ const ontologyID = (key: string): ontology.ID =>
24
+ new ontology.ID({ type: "range", key });
19
25
 
20
26
  export class Range {
21
27
  key: string;
@@ -25,6 +31,7 @@ export class Range {
25
31
  readonly channels: ChannelRetriever;
26
32
  private readonly aliaser: Aliaser;
27
33
  private readonly frameClient: framer.Client;
34
+ private readonly labelClient: label.Client;
28
35
 
29
36
  constructor(
30
37
  name: string,
@@ -34,6 +41,7 @@ export class Range {
34
41
  _kv: KV,
35
42
  _aliaser: Aliaser,
36
43
  _channels: ChannelRetriever,
44
+ _labelClient: label.Client,
37
45
  ) {
38
46
  this.key = key;
39
47
  this.name = name;
@@ -42,6 +50,7 @@ export class Range {
42
50
  this.kv = _kv;
43
51
  this.aliaser = _aliaser;
44
52
  this.channels = _channels;
53
+ this.labelClient = _labelClient;
45
54
  }
46
55
 
47
56
  async setAlias(channel: Key | Name, alias: string): Promise<void> {
@@ -60,7 +69,7 @@ export class Range {
60
69
  return await this.aliaser.list();
61
70
  }
62
71
 
63
- async openAliasTracker(): Promise<cdc.Observable<string, Alias>> {
72
+ async openAliasTracker(): Promise<signals.Observable<string, Alias>> {
64
73
  return await this.aliaser.openChangeTracker();
65
74
  }
66
75
 
@@ -71,4 +80,16 @@ export class Range {
71
80
  async read(channels: Params): Promise<Series | framer.Frame> {
72
81
  return await this.frameClient.read(this.timeRange, channels);
73
82
  }
83
+
84
+ async labels(): Promise<Label[]> {
85
+ return await this.labelClient.retrieveFor(ontologyID(this.key));
86
+ }
87
+
88
+ async addLabel(...labels: label.Key[]): Promise<void> {
89
+ await this.labelClient.label(ontologyID(this.key), labels);
90
+ }
91
+
92
+ async removeLabel(...labels: label.Key[]): Promise<void> {
93
+ await this.labelClient.removeLabels(ontologyID(this.key), labels);
94
+ }
74
95
  }
@@ -164,4 +164,38 @@ describe("Ranger", () => {
164
164
  });
165
165
  });
166
166
  });
167
+
168
+ describe("Labels", () => {
169
+ describe("set", () => {
170
+ it("should set a label on a range", async () => {
171
+ const rng = await client.ranges.create({
172
+ name: "My New One Second Range",
173
+ timeRange: TimeStamp.now().spanRange(TimeSpan.seconds(1)),
174
+ });
175
+ const lbl = await client.labels.create({
176
+ name: "My New Label",
177
+ color: "#E774D0",
178
+ });
179
+ await rng.addLabel(lbl.key);
180
+ const retrieved = await rng.labels();
181
+ expect(retrieved).toEqual([lbl]);
182
+ });
183
+ });
184
+ describe("remove", () => {
185
+ it("should remove a label from a range", async () => {
186
+ const rng = await client.ranges.create({
187
+ name: "My New One Second Range",
188
+ timeRange: TimeStamp.now().spanRange(TimeSpan.seconds(1)),
189
+ });
190
+ const lbl = await client.labels.create({
191
+ name: "My New Label",
192
+ color: "#E774D0",
193
+ });
194
+ await rng.addLabel(lbl.key);
195
+ await rng.removeLabel(lbl.key);
196
+ const retrieved = await rng.labels();
197
+ expect(retrieved).toEqual([]);
198
+ });
199
+ });
200
+ });
167
201
  });
@@ -7,4 +7,4 @@
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
- export * from "@/cdc/observable";
10
+ export * from "@/signals/observable";
@@ -0,0 +1,10 @@
1
+ // Copyright 2023 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
+
10
+ export * as signals from "@/signals/external";
@@ -43,8 +43,7 @@ export class Retriever {
43
43
 
44
44
  async retrieve(params: Params): Promise<Workspace[]> {
45
45
  const normalized = toArray(params);
46
- const res = await this.execute({ keys: normalized });
47
- return res;
46
+ return await this.execute({ keys: normalized });
48
47
  }
49
48
 
50
49
  async retrieveByAuthor(author: string): Promise<Workspace[]> {
@@ -46,7 +46,7 @@ const setLayoutReqZ = z.object({
46
46
 
47
47
  const setLayoutResZ = z.object({});
48
48
 
49
- export type Response = z.infer<typeof createResZ>;
49
+ export type CreateResponse = z.infer<typeof createResZ>;
50
50
 
51
51
  const CREATE_ENDPOINT = "/workspace/create";
52
52
  const DELETE_ENDPOINT = "/workspace/delete";
File without changes
File without changes