@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 +8 -8
- package/src/client.ts +4 -0
- package/src/control/authority.ts +1 -0
- package/src/control/state.ts +17 -1
- package/src/errors.ts +12 -2
- package/src/index.ts +1 -0
- package/src/label/client.ts +103 -0
- package/src/label/external.ts +13 -0
- package/src/{cdc → label}/index.ts +1 -1
- package/src/label/label.spec.ts +51 -0
- package/src/label/payload.ts +29 -0
- package/src/label/retriever.ts +65 -0
- package/src/label/writer.ts +90 -0
- package/src/ontology/client.ts +2 -1
- package/src/ontology/external.ts +1 -1
- package/src/ontology/ontology.spec.ts +1 -1
- package/src/ontology/payload.ts +2 -5
- package/src/ranger/alias.ts +4 -4
- package/src/ranger/client.ts +7 -2
- package/src/ranger/payload.ts +1 -1
- package/src/ranger/range.ts +23 -2
- package/src/ranger/ranger.spec.ts +34 -0
- package/src/{cdc → signals}/external.ts +1 -1
- package/src/signals/index.ts +10 -0
- package/src/workspace/retriever.ts +1 -2
- package/src/workspace/writer.ts +1 -1
- /package/src/ontology/{cdc.ts → signals.ts} +0 -0
- /package/src/{cdc → signals}/observable.ts +0 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synnaxlabs/client",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.13.
|
|
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/
|
|
21
|
-
"@synnaxlabs/
|
|
20
|
+
"@synnaxlabs/freighter": "0.4.1",
|
|
21
|
+
"@synnaxlabs/x": "0.8.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@types/node": "^20.
|
|
25
|
-
"@vitest/coverage-v8": "^
|
|
26
|
-
"typescript": "^5.
|
|
27
|
-
"vite": "^
|
|
28
|
-
"vitest": "^
|
|
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
|
}
|
package/src/control/authority.ts
CHANGED
package/src/control/state.ts
CHANGED
|
@@ -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 {
|
|
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
|
@@ -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";
|
|
@@ -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
|
+
}
|
package/src/ontology/client.ts
CHANGED
|
@@ -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;
|
package/src/ontology/external.ts
CHANGED
|
@@ -77,7 +77,7 @@ describe("Ontology", () => {
|
|
|
77
77
|
expect(newRootLength).toEqual(oldRootLength - 1);
|
|
78
78
|
});
|
|
79
79
|
});
|
|
80
|
-
describe("
|
|
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");
|
package/src/ontology/payload.ts
CHANGED
|
@@ -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
|
};
|
package/src/ranger/alias.ts
CHANGED
|
@@ -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<
|
|
140
|
-
return await
|
|
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):
|
|
166
|
+
(rangeKey: Key): signals.Decoder<string, Alias> =>
|
|
167
167
|
(variant, data) => {
|
|
168
168
|
if (variant === "delete") {
|
|
169
169
|
return data
|
package/src/ranger/client.ts
CHANGED
|
@@ -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
|
}
|
package/src/ranger/payload.ts
CHANGED
package/src/ranger/range.ts
CHANGED
|
@@ -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<
|
|
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
|
});
|
|
@@ -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
|
-
|
|
47
|
-
return res;
|
|
46
|
+
return await this.execute({ keys: normalized });
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
async retrieveByAuthor(author: string): Promise<Workspace[]> {
|
package/src/workspace/writer.ts
CHANGED
|
@@ -46,7 +46,7 @@ const setLayoutReqZ = z.object({
|
|
|
46
46
|
|
|
47
47
|
const setLayoutResZ = z.object({});
|
|
48
48
|
|
|
49
|
-
export type
|
|
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
|