@synnaxlabs/client 0.47.0 → 0.49.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 (311) hide show
  1. package/.turbo/turbo-build.log +7 -7
  2. package/dist/client.cjs +35 -35
  3. package/dist/client.js +6264 -5984
  4. package/dist/eslint.config.d.ts +3 -2
  5. package/dist/eslint.config.d.ts.map +1 -1
  6. package/dist/src/access/client.d.ts +3 -1
  7. package/dist/src/access/client.d.ts.map +1 -1
  8. package/dist/src/access/enforce.d.ts +35 -0
  9. package/dist/src/access/enforce.d.ts.map +1 -0
  10. package/dist/src/access/enforce.spec.d.ts +2 -0
  11. package/dist/src/access/enforce.spec.d.ts.map +1 -0
  12. package/dist/src/access/external.d.ts +3 -0
  13. package/dist/src/access/external.d.ts.map +1 -1
  14. package/dist/src/access/payload.d.ts +0 -6
  15. package/dist/src/access/payload.d.ts.map +1 -1
  16. package/dist/src/access/policy/access.spec.d.ts +2 -0
  17. package/dist/src/access/policy/access.spec.d.ts.map +1 -0
  18. package/dist/src/access/policy/client.d.ts +485 -31
  19. package/dist/src/access/policy/client.d.ts.map +1 -1
  20. package/dist/src/access/policy/payload.d.ts +36 -113
  21. package/dist/src/access/policy/payload.d.ts.map +1 -1
  22. package/dist/src/access/role/client.d.ts +135 -0
  23. package/dist/src/access/role/client.d.ts.map +1 -0
  24. package/dist/src/access/role/external.d.ts.map +1 -0
  25. package/dist/src/access/role/index.d.ts +2 -0
  26. package/dist/src/access/role/index.d.ts.map +1 -0
  27. package/dist/src/access/role/payload.d.ts +27 -0
  28. package/dist/src/access/role/payload.d.ts.map +1 -0
  29. package/dist/src/access/role/role.spec.d.ts +2 -0
  30. package/dist/src/access/role/role.spec.d.ts.map +1 -0
  31. package/dist/src/arc/access.spec.d.ts +2 -0
  32. package/dist/src/arc/access.spec.d.ts.map +1 -0
  33. package/dist/src/arc/client.d.ts +5 -14
  34. package/dist/src/arc/client.d.ts.map +1 -1
  35. package/dist/src/arc/payload.d.ts +11 -2
  36. package/dist/src/arc/payload.d.ts.map +1 -1
  37. package/dist/src/auth/auth.d.ts +5 -3
  38. package/dist/src/auth/auth.d.ts.map +1 -1
  39. package/dist/src/channel/access.spec.d.ts +2 -0
  40. package/dist/src/channel/access.spec.d.ts.map +1 -0
  41. package/dist/src/channel/client.d.ts +0 -1
  42. package/dist/src/channel/client.d.ts.map +1 -1
  43. package/dist/src/channel/payload.d.ts +19 -8
  44. package/dist/src/channel/payload.d.ts.map +1 -1
  45. package/dist/src/channel/payload.spec.d.ts +2 -0
  46. package/dist/src/channel/payload.spec.d.ts.map +1 -0
  47. package/dist/src/channel/retriever.d.ts +4 -6
  48. package/dist/src/channel/retriever.d.ts.map +1 -1
  49. package/dist/src/channel/writer.d.ts.map +1 -1
  50. package/dist/src/client.d.ts +18 -10
  51. package/dist/src/client.d.ts.map +1 -1
  52. package/dist/src/connection/checker.d.ts +2 -3
  53. package/dist/src/connection/checker.d.ts.map +1 -1
  54. package/dist/src/connection.spec.d.ts +2 -0
  55. package/dist/src/connection.spec.d.ts.map +1 -0
  56. package/dist/src/device/access.spec.d.ts +2 -0
  57. package/dist/src/device/access.spec.d.ts.map +1 -0
  58. package/dist/src/{hardware/device → device}/client.d.ts +14 -7
  59. package/dist/src/device/client.d.ts.map +1 -0
  60. package/dist/src/device/device.spec.d.ts.map +1 -0
  61. package/dist/src/device/external.d.ts.map +1 -0
  62. package/dist/src/device/index.d.ts.map +1 -0
  63. package/dist/src/{hardware/device → device}/payload.d.ts +1 -1
  64. package/dist/src/device/payload.d.ts.map +1 -0
  65. package/dist/src/errors.d.ts +3 -0
  66. package/dist/src/errors.d.ts.map +1 -1
  67. package/dist/src/framer/adapter.d.ts +2 -2
  68. package/dist/src/framer/adapter.d.ts.map +1 -1
  69. package/dist/src/framer/client.d.ts +8 -1
  70. package/dist/src/framer/client.d.ts.map +1 -1
  71. package/dist/src/framer/frame.d.ts +11 -5
  72. package/dist/src/framer/frame.d.ts.map +1 -1
  73. package/dist/src/framer/iterator.d.ts +3 -3
  74. package/dist/src/framer/streamer.d.ts +24 -21
  75. package/dist/src/framer/streamer.d.ts.map +1 -1
  76. package/dist/src/framer/writer.d.ts +13 -13
  77. package/dist/src/index.d.ts +6 -7
  78. package/dist/src/index.d.ts.map +1 -1
  79. package/dist/src/label/access.spec.d.ts +2 -0
  80. package/dist/src/label/access.spec.d.ts.map +1 -0
  81. package/dist/src/label/client.d.ts +20 -11
  82. package/dist/src/label/client.d.ts.map +1 -1
  83. package/dist/src/ontology/client.d.ts +6 -6
  84. package/dist/src/ontology/client.d.ts.map +1 -1
  85. package/dist/src/ontology/group/access.spec.d.ts +2 -0
  86. package/dist/src/ontology/group/access.spec.d.ts.map +1 -0
  87. package/dist/src/ontology/group/client.d.ts +2 -2
  88. package/dist/src/ontology/group/client.d.ts.map +1 -1
  89. package/dist/src/ontology/group/payload.d.ts +1 -2
  90. package/dist/src/ontology/group/payload.d.ts.map +1 -1
  91. package/dist/src/ontology/payload.d.ts +23 -17
  92. package/dist/src/ontology/payload.d.ts.map +1 -1
  93. package/dist/src/ontology/writer.d.ts +10 -10
  94. package/dist/src/ontology/writer.d.ts.map +1 -1
  95. package/dist/src/rack/access.spec.d.ts +2 -0
  96. package/dist/src/rack/access.spec.d.ts.map +1 -0
  97. package/dist/src/{hardware/rack → rack}/client.d.ts +15 -8
  98. package/dist/src/rack/client.d.ts.map +1 -0
  99. package/dist/src/rack/external.d.ts.map +1 -0
  100. package/dist/src/rack/index.d.ts.map +1 -0
  101. package/dist/src/{hardware/rack → rack}/payload.d.ts +1 -1
  102. package/dist/src/rack/payload.d.ts.map +1 -0
  103. package/dist/src/rack/rack.spec.d.ts.map +1 -0
  104. package/dist/src/ranger/access.spec.d.ts +2 -0
  105. package/dist/src/ranger/access.spec.d.ts.map +1 -0
  106. package/dist/src/ranger/alias.d.ts +1 -8
  107. package/dist/src/ranger/alias.d.ts.map +1 -1
  108. package/dist/src/ranger/client.d.ts +12 -5
  109. package/dist/src/ranger/client.d.ts.map +1 -1
  110. package/dist/src/ranger/kv.d.ts +0 -3
  111. package/dist/src/ranger/kv.d.ts.map +1 -1
  112. package/dist/src/ranger/writer.d.ts +2 -2
  113. package/dist/src/ranger/writer.d.ts.map +1 -1
  114. package/dist/src/status/access.spec.d.ts +2 -0
  115. package/dist/src/status/access.spec.d.ts.map +1 -0
  116. package/dist/src/status/client.d.ts +4 -4
  117. package/dist/src/status/client.d.ts.map +1 -1
  118. package/dist/src/status/payload.d.ts +9 -2
  119. package/dist/src/status/payload.d.ts.map +1 -1
  120. package/dist/src/task/access.spec.d.ts +2 -0
  121. package/dist/src/task/access.spec.d.ts.map +1 -0
  122. package/dist/src/{hardware/task → task}/client.d.ts +26 -15
  123. package/dist/src/task/client.d.ts.map +1 -0
  124. package/dist/src/task/external.d.ts +3 -0
  125. package/dist/src/task/external.d.ts.map +1 -0
  126. package/dist/src/task/index.d.ts.map +1 -0
  127. package/dist/src/{hardware/task → task}/payload.d.ts +45 -6
  128. package/dist/src/task/payload.d.ts.map +1 -0
  129. package/dist/src/task/task.spec.d.ts.map +1 -0
  130. package/dist/src/testutil/access.d.ts +4 -0
  131. package/dist/src/testutil/access.d.ts.map +1 -0
  132. package/dist/src/testutil/client.d.ts +3 -3
  133. package/dist/src/testutil/client.d.ts.map +1 -1
  134. package/dist/src/transport.d.ts.map +1 -1
  135. package/dist/src/user/access.spec.d.ts +2 -0
  136. package/dist/src/user/access.spec.d.ts.map +1 -0
  137. package/dist/src/user/client.d.ts +10 -1
  138. package/dist/src/user/client.d.ts.map +1 -1
  139. package/dist/src/user/external.d.ts +1 -1
  140. package/dist/src/user/external.d.ts.map +1 -1
  141. package/dist/src/user/payload.d.ts.map +1 -1
  142. package/dist/src/workspace/access.spec.d.ts +2 -0
  143. package/dist/src/workspace/access.spec.d.ts.map +1 -0
  144. package/dist/src/workspace/client.d.ts +10 -5
  145. package/dist/src/workspace/client.d.ts.map +1 -1
  146. package/dist/src/workspace/lineplot/access.spec.d.ts +2 -0
  147. package/dist/src/workspace/lineplot/access.spec.d.ts.map +1 -0
  148. package/dist/src/workspace/lineplot/client.d.ts +8 -1
  149. package/dist/src/workspace/lineplot/client.d.ts.map +1 -1
  150. package/dist/src/workspace/log/access.spec.d.ts +2 -0
  151. package/dist/src/workspace/log/access.spec.d.ts.map +1 -0
  152. package/dist/src/workspace/log/client.d.ts +8 -1
  153. package/dist/src/workspace/log/client.d.ts.map +1 -1
  154. package/dist/src/workspace/schematic/access.spec.d.ts +2 -0
  155. package/dist/src/workspace/schematic/access.spec.d.ts.map +1 -0
  156. package/dist/src/workspace/schematic/client.d.ts +8 -1
  157. package/dist/src/workspace/schematic/client.d.ts.map +1 -1
  158. package/dist/src/workspace/schematic/symbol/access.spec.d.ts +2 -0
  159. package/dist/src/workspace/schematic/symbol/access.spec.d.ts.map +1 -0
  160. package/dist/src/workspace/schematic/symbol/client.d.ts +1 -5
  161. package/dist/src/workspace/schematic/symbol/client.d.ts.map +1 -1
  162. package/dist/src/workspace/schematic/symbol/payload.d.ts +2 -2
  163. package/dist/src/workspace/table/access.spec.d.ts +2 -0
  164. package/dist/src/workspace/table/access.spec.d.ts.map +1 -0
  165. package/dist/src/workspace/table/client.d.ts +8 -1
  166. package/dist/src/workspace/table/client.d.ts.map +1 -1
  167. package/eslint.config.ts +3 -1
  168. package/package.json +8 -8
  169. package/src/access/client.ts +5 -2
  170. package/src/access/enforce.spec.ts +189 -0
  171. package/src/access/enforce.ts +84 -0
  172. package/src/access/external.ts +3 -0
  173. package/src/access/payload.ts +1 -13
  174. package/src/access/policy/access.spec.ts +147 -0
  175. package/src/access/policy/client.ts +21 -25
  176. package/src/access/policy/payload.ts +9 -5
  177. package/src/access/role/client.ts +135 -0
  178. package/src/access/role/external.ts +11 -0
  179. package/src/{hardware → access/role}/index.ts +1 -1
  180. package/src/access/role/payload.ts +32 -0
  181. package/src/access/role/role.spec.ts +95 -0
  182. package/src/arc/access.spec.ts +143 -0
  183. package/src/arc/client.ts +7 -31
  184. package/src/arc/payload.ts +4 -0
  185. package/src/auth/auth.spec.ts +13 -13
  186. package/src/auth/auth.ts +33 -11
  187. package/src/channel/access.spec.ts +116 -0
  188. package/src/channel/channel.spec.ts +63 -73
  189. package/src/channel/client.ts +2 -8
  190. package/src/channel/payload.spec.ts +171 -0
  191. package/src/channel/payload.ts +37 -8
  192. package/src/channel/retriever.ts +10 -11
  193. package/src/channel/writer.ts +3 -7
  194. package/src/client.ts +38 -28
  195. package/src/connection/checker.ts +10 -10
  196. package/src/connection/connection.spec.ts +13 -13
  197. package/src/connection.spec.ts +145 -0
  198. package/src/device/access.spec.ts +159 -0
  199. package/src/{hardware/device → device}/client.ts +12 -21
  200. package/src/{hardware/device → device}/device.spec.ts +70 -34
  201. package/src/device/external.ts +11 -0
  202. package/src/{hardware/rack → device}/index.ts +1 -1
  203. package/src/{hardware/device → device}/payload.ts +3 -3
  204. package/src/errors.ts +2 -0
  205. package/src/framer/adapter.spec.ts +351 -13
  206. package/src/framer/adapter.ts +23 -13
  207. package/src/framer/client.spec.ts +14 -20
  208. package/src/framer/client.ts +3 -5
  209. package/src/framer/deleter.spec.ts +1 -1
  210. package/src/framer/frame.spec.ts +427 -0
  211. package/src/framer/frame.ts +30 -3
  212. package/src/framer/iterator.ts +4 -4
  213. package/src/framer/streamer.spec.ts +155 -10
  214. package/src/framer/streamer.ts +35 -12
  215. package/src/framer/writer.spec.ts +5 -5
  216. package/src/index.ts +13 -7
  217. package/src/label/access.spec.ts +109 -0
  218. package/src/label/client.ts +10 -14
  219. package/src/ontology/client.ts +4 -6
  220. package/src/ontology/group/access.spec.ts +77 -0
  221. package/src/ontology/group/client.ts +3 -7
  222. package/src/ontology/group/group.spec.ts +18 -0
  223. package/src/ontology/group/payload.ts +2 -2
  224. package/src/ontology/ontology.spec.ts +2 -0
  225. package/src/ontology/payload.ts +18 -2
  226. package/src/ontology/writer.ts +3 -7
  227. package/src/rack/access.spec.ts +102 -0
  228. package/src/{hardware/rack → rack}/client.ts +14 -19
  229. package/src/{hardware/device/index.ts → rack/external.ts} +2 -1
  230. package/src/{hardware/external.ts → rack/index.ts} +1 -1
  231. package/src/{hardware/rack → rack}/payload.ts +2 -2
  232. package/src/{hardware/rack → rack}/rack.spec.ts +43 -17
  233. package/src/ranger/access.spec.ts +115 -0
  234. package/src/ranger/alias.ts +6 -14
  235. package/src/ranger/client.ts +13 -14
  236. package/src/ranger/kv.ts +7 -9
  237. package/src/ranger/ranger.spec.ts +4 -4
  238. package/src/ranger/writer.ts +3 -7
  239. package/src/status/access.spec.ts +129 -0
  240. package/src/status/client.ts +5 -9
  241. package/src/status/payload.ts +3 -2
  242. package/src/task/access.spec.ts +131 -0
  243. package/src/{hardware/task → task}/client.ts +50 -25
  244. package/src/task/external.ts +11 -0
  245. package/src/{hardware/task → task}/index.ts +1 -1
  246. package/src/{hardware/task → task}/payload.ts +22 -3
  247. package/src/{hardware/task → task}/task.spec.ts +197 -34
  248. package/src/testutil/access.ts +34 -0
  249. package/src/testutil/channels.ts +3 -3
  250. package/src/testutil/client.ts +4 -4
  251. package/src/transport.ts +1 -3
  252. package/src/user/access.spec.ts +107 -0
  253. package/src/user/client.ts +10 -12
  254. package/src/user/external.ts +12 -1
  255. package/src/user/payload.ts +3 -5
  256. package/src/workspace/access.spec.ts +108 -0
  257. package/src/workspace/client.ts +11 -27
  258. package/src/workspace/lineplot/access.spec.ts +134 -0
  259. package/src/workspace/lineplot/client.ts +8 -13
  260. package/src/workspace/log/access.spec.ts +134 -0
  261. package/src/workspace/log/client.ts +8 -13
  262. package/src/workspace/schematic/access.spec.ts +134 -0
  263. package/src/workspace/schematic/client.ts +9 -18
  264. package/src/workspace/schematic/symbol/access.spec.ts +172 -0
  265. package/src/workspace/schematic/symbol/client.ts +6 -17
  266. package/src/workspace/schematic/symbol/payload.ts +1 -1
  267. package/src/workspace/table/access.spec.ts +134 -0
  268. package/src/workspace/table/client.ts +8 -13
  269. package/dist/src/access/policy/policy.spec.d.ts +0 -2
  270. package/dist/src/access/policy/policy.spec.d.ts.map +0 -1
  271. package/dist/src/hardware/client.d.ts +0 -10
  272. package/dist/src/hardware/client.d.ts.map +0 -1
  273. package/dist/src/hardware/device/client.d.ts.map +0 -1
  274. package/dist/src/hardware/device/device.spec.d.ts.map +0 -1
  275. package/dist/src/hardware/device/external.d.ts.map +0 -1
  276. package/dist/src/hardware/device/index.d.ts.map +0 -1
  277. package/dist/src/hardware/device/payload.d.ts.map +0 -1
  278. package/dist/src/hardware/external.d.ts +0 -2
  279. package/dist/src/hardware/external.d.ts.map +0 -1
  280. package/dist/src/hardware/index.d.ts +0 -2
  281. package/dist/src/hardware/index.d.ts.map +0 -1
  282. package/dist/src/hardware/rack/client.d.ts.map +0 -1
  283. package/dist/src/hardware/rack/external.d.ts.map +0 -1
  284. package/dist/src/hardware/rack/index.d.ts.map +0 -1
  285. package/dist/src/hardware/rack/payload.d.ts.map +0 -1
  286. package/dist/src/hardware/rack/rack.spec.d.ts.map +0 -1
  287. package/dist/src/hardware/task/client.d.ts.map +0 -1
  288. package/dist/src/hardware/task/external.d.ts.map +0 -1
  289. package/dist/src/hardware/task/index.d.ts.map +0 -1
  290. package/dist/src/hardware/task/payload.d.ts.map +0 -1
  291. package/dist/src/hardware/task/task.spec.d.ts.map +0 -1
  292. package/dist/src/user/retriever.d.ts +0 -16
  293. package/dist/src/user/retriever.d.ts.map +0 -1
  294. package/dist/src/user/writer.d.ts +0 -11
  295. package/dist/src/user/writer.d.ts.map +0 -1
  296. package/src/access/policy/policy.spec.ts +0 -329
  297. package/src/hardware/client.ts +0 -24
  298. package/src/hardware/device/external.ts +0 -11
  299. package/src/hardware/rack/external.ts +0 -11
  300. package/src/hardware/task/external.ts +0 -11
  301. package/src/user/retriever.ts +0 -41
  302. package/src/user/writer.ts +0 -84
  303. /package/dist/src/{hardware/device → access/role}/external.d.ts +0 -0
  304. /package/dist/src/{hardware/device → device}/device.spec.d.ts +0 -0
  305. /package/dist/src/{hardware/rack → device}/external.d.ts +0 -0
  306. /package/dist/src/{hardware/device → device}/index.d.ts +0 -0
  307. /package/dist/src/{hardware/task → rack}/external.d.ts +0 -0
  308. /package/dist/src/{hardware/rack → rack}/index.d.ts +0 -0
  309. /package/dist/src/{hardware/rack → rack}/rack.spec.d.ts +0 -0
  310. /package/dist/src/{hardware/task → task}/index.d.ts +0 -0
  311. /package/dist/src/{hardware/task → task}/task.spec.d.ts +0 -0
@@ -0,0 +1,171 @@
1
+ // Copyright 2025 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 { DataType } from "@synnaxlabs/x";
11
+ import { describe, expect, it } from "vitest";
12
+
13
+ import { escapeInvalidName, nameZ, newZ } from "@/channel/payload";
14
+
15
+ describe("nameZ", () => {
16
+ describe("valid names", () => {
17
+ const validNames = [
18
+ ["temperature", "lowercase letters"],
19
+ ["Pressure", "capitalized word"],
20
+ ["sensor1", "letters followed by digit"],
21
+ ["sensor_temp", "letters with underscore"],
22
+ ["temp123", "letters followed by digits"],
23
+ ["Sensor_temp", "capitalized with underscore"],
24
+ ["temp123_sensor_temp", "complex valid name"],
25
+ ["_private", "underscore prefix"],
26
+ ["__double", "double underscore prefix"],
27
+ ["a", "single letter"],
28
+ ["A", "single capital letter"],
29
+ ["_", "single underscore"],
30
+ ["_1", "underscore followed by digit"],
31
+ ];
32
+ validNames.forEach(([name, description]) => {
33
+ it(`should accept ${name} (${description})`, () => {
34
+ const result = nameZ.safeParse(name);
35
+ expect(result.success).toBe(true);
36
+ });
37
+ });
38
+ });
39
+
40
+ describe("invalid names", () => {
41
+ it("should reject empty string", () => {
42
+ const result = nameZ.safeParse("");
43
+ expect(result.success).toBe(false);
44
+ expect(result.error?.issues[0].message).toContain("Name must not be empty");
45
+ });
46
+
47
+ it("should reject name starting with digit", () => {
48
+ const result = nameZ.safeParse("1sensor");
49
+ expect(result.success).toBe(false);
50
+ // Regex validation covers both "cannot start with digit" and "invalid characters"
51
+ expect(result.error?.issues[0].message).toContain(
52
+ "can only contain letters, digits, and underscores",
53
+ );
54
+ });
55
+
56
+ it("should reject name with spaces", () => {
57
+ const result = nameZ.safeParse("my channel");
58
+ expect(result.success).toBe(false);
59
+ expect(result.error?.issues[0].message).toContain(
60
+ "can only contain letters, digits, and underscores",
61
+ );
62
+ });
63
+
64
+ it("should reject name with special characters", () => {
65
+ const result = nameZ.safeParse("sensor!");
66
+ expect(result.success).toBe(false);
67
+ });
68
+
69
+ it("should reject name with hyphens", () => {
70
+ const result = nameZ.safeParse("sensor-temp");
71
+ expect(result.success).toBe(false);
72
+ });
73
+
74
+ it("should reject name with dots", () => {
75
+ const result = nameZ.safeParse("sensor.temp");
76
+ expect(result.success).toBe(false);
77
+ });
78
+
79
+ it("should reject name with parentheses", () => {
80
+ const result = nameZ.safeParse("sensor(1)");
81
+ expect(result.success).toBe(false);
82
+ });
83
+
84
+ it("should reject name with brackets", () => {
85
+ const result = nameZ.safeParse("sensor[0]");
86
+ expect(result.success).toBe(false);
87
+ });
88
+ });
89
+ });
90
+
91
+ describe("newZ", () => {
92
+ const validNewChannel = {
93
+ name: "temperature_sensor",
94
+ dataType: DataType.FLOAT32,
95
+ virtual: true,
96
+ };
97
+
98
+ describe("name validation", () => {
99
+ it("should accept valid channel names", () => {
100
+ const result = newZ.safeParse(validNewChannel);
101
+ expect(result.success).toBe(true);
102
+ });
103
+
104
+ it("should reject empty name", () => {
105
+ const result = newZ.safeParse({ ...validNewChannel, name: "" });
106
+ expect(result.success).toBe(false);
107
+ });
108
+
109
+ it("should reject name starting with digit", () => {
110
+ const result = newZ.safeParse({ ...validNewChannel, name: "1sensor" });
111
+ expect(result.success).toBe(false);
112
+ });
113
+
114
+ it("should reject name with spaces", () => {
115
+ const result = newZ.safeParse({ ...validNewChannel, name: "my channel" });
116
+ expect(result.success).toBe(false);
117
+ });
118
+
119
+ it("should reject name with special characters", () => {
120
+ const result = newZ.safeParse({ ...validNewChannel, name: "sensor-temp" });
121
+ expect(result.success).toBe(false);
122
+ });
123
+
124
+ it("should accept name with underscores", () => {
125
+ const result = newZ.safeParse({
126
+ ...validNewChannel,
127
+ name: "sensor_temp_123",
128
+ });
129
+ expect(result.success).toBe(true);
130
+ });
131
+
132
+ it("should accept name starting with underscore", () => {
133
+ const result = newZ.safeParse({ ...validNewChannel, name: "_private_sensor" });
134
+ expect(result.success).toBe(true);
135
+ });
136
+ });
137
+ });
138
+ describe("escapeInvalidName", () => {
139
+ it("should escape invalid name", () => {
140
+ const result = escapeInvalidName("sensor-temp");
141
+ expect(result).toBe("sensor_temp");
142
+ });
143
+ it("should escape name starting with digit", () => {
144
+ const result = escapeInvalidName("1sensor");
145
+ expect(result).toBe("_1sensor");
146
+ });
147
+ it("should escape name with spaces", () => {
148
+ const result = escapeInvalidName("my channel");
149
+ expect(result).toBe("my_channel");
150
+ });
151
+ it("should escape name with special characters", () => {
152
+ const result = escapeInvalidName("sensor!");
153
+ expect(result).toBe("sensor_");
154
+ });
155
+ it("should escape name with hyphens", () => {
156
+ const result = escapeInvalidName("sensor-temp");
157
+ expect(result).toBe("sensor_temp");
158
+ });
159
+ it("should escape name with dots", () => {
160
+ const result = escapeInvalidName("sensor.temp");
161
+ expect(result).toBe("sensor_temp");
162
+ });
163
+ it("should allow an empty string by default", () => {
164
+ const result = escapeInvalidName("");
165
+ expect(result).toBe("");
166
+ });
167
+ it("should change empty string to underscore when changeEmptyToUnderscore is true", () => {
168
+ const result = escapeInvalidName("", true);
169
+ expect(result).toBe("_");
170
+ });
171
+ });
@@ -18,6 +18,8 @@ import {
18
18
  } from "@synnaxlabs/x";
19
19
  import { z } from "zod";
20
20
 
21
+ import { ontology } from "@/ontology";
22
+
21
23
  const errorMessage = "Channel key must be a valid uint32.";
22
24
  export const keyZ = z.uint32().or(
23
25
  z
@@ -28,7 +30,14 @@ export const keyZ = z.uint32().or(
28
30
  );
29
31
  export type Key = z.infer<typeof keyZ>;
30
32
  export type Keys = Key[];
31
- export const nameZ = z.string();
33
+ const VALID_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
34
+ export const nameZ = z
35
+ .string()
36
+ .min(1, "Name must not be empty")
37
+ .regex(
38
+ VALID_NAME_PATTERN,
39
+ "Name can only contain letters, digits, and underscores, and cannot start with a digit",
40
+ );
32
41
  export type Name = z.infer<typeof nameZ>;
33
42
  export type Names = Name[];
34
43
  export type KeyOrName = Key | Name;
@@ -50,10 +59,10 @@ export type Operation = z.infer<typeof operationZ>;
50
59
  export const statusZ = status.statusZ();
51
60
  export type Status = z.infer<typeof statusZ>;
52
61
  export const payloadZ = z.object({
53
- name: nameZ,
62
+ name: z.string(),
54
63
  key: keyZ,
55
64
  dataType: DataType.z,
56
- leaseholder: z.number(),
65
+ leaseholder: zod.uint12,
57
66
  index: keyZ,
58
67
  isIndex: z.boolean(),
59
68
  internal: z.boolean(),
@@ -66,15 +75,17 @@ export const payloadZ = z.object({
66
75
  });
67
76
  export interface Payload extends z.infer<typeof payloadZ> {}
68
77
 
69
- export const newZ = payloadZ.omit({ requires: true }).extend({
78
+ export const newZ = payloadZ.extend({
70
79
  key: keyZ.optional(),
71
- leaseholder: z.number().optional(),
80
+ name: nameZ,
81
+ leaseholder: zod.uint12.optional(),
72
82
  index: keyZ.optional(),
73
83
  isIndex: z.boolean().optional(),
74
- internal: z.boolean().optional().default(false),
75
- virtual: z.boolean().optional().default(false),
76
- expression: z.string().optional().default(""),
84
+ internal: z.boolean().default(false),
85
+ virtual: z.boolean().default(false),
86
+ expression: z.string().default(""),
77
87
  operations: array.nullableZ(operationZ).optional(),
88
+ requires: array.nullableZ(keyZ).optional(),
78
89
  });
79
90
 
80
91
  export interface New
@@ -88,3 +99,21 @@ export const paramsZ = z.union([
88
99
  zod.toArray(payloadZ).transform((p) => p.map((c) => c.key)),
89
100
  ]);
90
101
  export type Params = Key | Name | Keys | Names | Payload | Payload[];
102
+
103
+ export const ontologyID = ontology.createIDFactory<Key>("channel");
104
+ export const TYPE_ONTOLOGY_ID = ontologyID(0);
105
+
106
+ const CHAR_REGEX = /[a-zA-Z0-9_]/;
107
+
108
+ export const escapeInvalidName = (name: string, changeEmptyToUnderscore = false) => {
109
+ if (name === "") return changeEmptyToUnderscore ? "_" : "";
110
+ if (name.match(VALID_NAME_PATTERN)) return name;
111
+ // if it doesn't match, convert non-alphanumeric characters to underscores and prepend
112
+ // an underscore if the first character is a digit
113
+ let result = "";
114
+ for (const char of name)
115
+ if (char.match(CHAR_REGEX)) result += char;
116
+ else result += "_";
117
+ if (result[0].match(/^\d/)) result = `_${result}`;
118
+ return result;
119
+ };
@@ -7,8 +7,8 @@
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";
11
- import { array, DataType, debounce } from "@synnaxlabs/x";
10
+ import { sendRequired, type UnaryClient } from "@synnaxlabs/freighter";
11
+ import { array, DataType, debounce, zod } from "@synnaxlabs/x";
12
12
  import { Mutex } from "async-mutex";
13
13
  import { z } from "zod";
14
14
 
@@ -25,25 +25,25 @@ import {
25
25
  payloadZ,
26
26
  } from "@/channel/payload";
27
27
  import { QueryError } from "@/errors";
28
+ import { keyZ as rangeKeyZ } from "@/ranger/payload";
28
29
  import {
29
30
  analyzeParams as analyzeParameters,
30
31
  type ParamAnalysisResult,
31
32
  } from "@/util/retrieve";
32
33
 
33
34
  const reqZ = z.object({
34
- leaseholder: z.number().optional(),
35
+ nodeKey: zod.uint12.optional(),
35
36
  keys: keyZ.array().optional(),
36
37
  names: z.string().array().optional(),
37
38
  searchTerm: z.string().optional(),
38
- rangeKey: z.string().optional(),
39
- limit: z.number().optional(),
40
- offset: z.number().optional(),
39
+ rangeKey: rangeKeyZ.optional(),
40
+ limit: z.int().optional(),
41
+ offset: z.int().optional(),
41
42
  dataTypes: DataType.z.array().optional(),
42
43
  notDataTypes: DataType.z.array().optional(),
43
44
  virtual: z.boolean().optional(),
44
45
  isIndex: z.boolean().optional(),
45
46
  internal: z.boolean().optional(),
46
- calculated: z.boolean().optional(),
47
47
  legacyCalculated: z.boolean().optional(),
48
48
  });
49
49
  export interface RetrieveRequest extends z.input<typeof reqZ> {}
@@ -72,7 +72,6 @@ export interface Retriever {
72
72
  }
73
73
 
74
74
  export class ClusterRetriever implements Retriever {
75
- private static readonly ENDPOINT = "/channel/retrieve";
76
75
  private readonly client: UnaryClient;
77
76
 
78
77
  constructor(client: UnaryClient) {
@@ -95,13 +94,13 @@ export class ClusterRetriever implements Retriever {
95
94
  }
96
95
 
97
96
  private async execute(request: RetrieveRequest): Promise<Payload[]> {
98
- const [res, err] = await this.client.send(
99
- ClusterRetriever.ENDPOINT,
97
+ const res = await sendRequired(
98
+ this.client,
99
+ "/channel/retrieve",
100
100
  request,
101
101
  reqZ,
102
102
  resZ,
103
103
  );
104
- if (err != null) throw err;
105
104
  return res.channels;
106
105
  }
107
106
  }
@@ -34,10 +34,6 @@ const deleteResZ = z.object({});
34
34
  const renameReqZ = z.object({ keys: keyZ.array(), names: nameZ.array() });
35
35
  const renameResZ = z.object({});
36
36
 
37
- const CREATE_ENDPOINT = "/channel/create";
38
- const DELETE_ENDPOINT = "/channel/delete";
39
- const RENAME_ENDPOINT = "/channel/rename";
40
-
41
37
  export interface DeleteProps extends z.input<typeof deleteReqZ> {}
42
38
  export interface RenameProps extends z.input<typeof renameReqZ> {}
43
39
 
@@ -56,7 +52,7 @@ export class Writer {
56
52
  typeof createResZ
57
53
  >(
58
54
  this.client,
59
- CREATE_ENDPOINT,
55
+ "/channel/create",
60
56
  {
61
57
  channels: channels.map((c) => ({
62
58
  ...c,
@@ -74,7 +70,7 @@ export class Writer {
74
70
  const keys = keyZ.array().parse(props.keys ?? []);
75
71
  await sendRequired<typeof deleteReqZ, typeof deleteResZ>(
76
72
  this.client,
77
- DELETE_ENDPOINT,
73
+ "/channel/delete",
78
74
  props,
79
75
  deleteReqZ,
80
76
  deleteResZ,
@@ -86,7 +82,7 @@ export class Writer {
86
82
  async rename(keys: Key[], names: string[]): Promise<void> {
87
83
  await sendRequired<typeof renameReqZ, typeof renameResZ>(
88
84
  this.client,
89
- RENAME_ENDPOINT,
85
+ "/channel/rename",
90
86
  { keys, names },
91
87
  renameReqZ,
92
88
  renameResZ,
package/src/client.ts CHANGED
@@ -16,21 +16,20 @@ import { auth } from "@/auth";
16
16
  import { channel } from "@/channel";
17
17
  import { connection } from "@/connection";
18
18
  import { control } from "@/control";
19
+ import { device } from "@/device";
19
20
  import { errorsMiddleware } from "@/errors";
20
21
  import { framer } from "@/framer";
21
- import { hardware } from "@/hardware";
22
- import { device } from "@/hardware/device";
23
- import { rack } from "@/hardware/rack";
24
- import { task } from "@/hardware/task";
25
22
  import { label } from "@/label";
26
23
  import { ontology } from "@/ontology";
24
+ import { rack } from "@/rack";
27
25
  import { ranger } from "@/ranger";
28
26
  import { status } from "@/status";
27
+ import { task } from "@/task";
29
28
  import { Transport } from "@/transport";
30
29
  import { user } from "@/user";
31
30
  import { workspace } from "@/workspace";
32
31
 
33
- export const synnaxPropsZ = z.object({
32
+ export const synnaxParamsZ = z.object({
34
33
  host: z.string({ error: "Host is required" }).min(1, "Host is required"),
35
34
  port: z
36
35
  .number({ error: "Port is required" })
@@ -38,13 +37,13 @@ export const synnaxPropsZ = z.object({
38
37
  username: z.string().min(1, "Username is required"),
39
38
  password: z.string().min(1, "Password is required"),
40
39
  connectivityPollFrequency: TimeSpan.z.default(TimeSpan.seconds(30)),
41
- secure: z.boolean().optional().default(false),
40
+ secure: z.boolean().default(false),
42
41
  name: z.string().optional(),
43
42
  retry: breaker.breakerConfigZ.optional(),
44
43
  });
45
44
 
46
- export interface SynnaxProps extends z.input<typeof synnaxPropsZ> {}
47
- export interface ParsedSynnaxProps extends z.infer<typeof synnaxPropsZ> {}
45
+ export interface SynnaxParams extends z.input<typeof synnaxParamsZ> {}
46
+ export interface ParsedSynnaxParams extends z.infer<typeof synnaxParamsZ> {}
48
47
 
49
48
  /**
50
49
  * Client to perform operations against a Synnax cluster.
@@ -56,10 +55,10 @@ export interface ParsedSynnaxProps extends z.infer<typeof synnaxPropsZ> {}
56
55
  */
57
56
  export default class Synnax extends framer.Client {
58
57
  readonly createdAt: TimeStamp;
59
- readonly props: ParsedSynnaxProps;
58
+ readonly params: ParsedSynnaxParams;
60
59
  readonly ranges: ranger.Client;
61
60
  readonly channels: channel.Client;
62
- readonly auth: auth.Client | undefined;
61
+ readonly auth: auth.Client;
63
62
  readonly users: user.Client;
64
63
  readonly access: access.Client;
65
64
  readonly connectivity: connection.Checker;
@@ -67,7 +66,9 @@ export default class Synnax extends framer.Client {
67
66
  readonly workspaces: workspace.Client;
68
67
  readonly labels: label.Client;
69
68
  readonly statuses: status.Client;
70
- readonly hardware: hardware.Client;
69
+ readonly tasks: task.Client;
70
+ readonly racks: rack.Client;
71
+ readonly devices: device.Client;
71
72
  readonly control: control.Client;
72
73
  readonly arcs: arc.Client;
73
74
  static readonly connectivity = connection.Checker;
@@ -93,8 +94,8 @@ export default class Synnax extends framer.Client {
93
94
  * A Synnax client must be closed when it is no longer needed. This will stop
94
95
  * the client from polling the cluster for connectivity information.
95
96
  */
96
- constructor(props_: SynnaxProps) {
97
- const props = synnaxPropsZ.parse(props_);
97
+ constructor(params: SynnaxParams) {
98
+ const parsedParams = synnaxParamsZ.parse(params);
98
99
  const {
99
100
  host,
100
101
  port,
@@ -103,33 +104,29 @@ export default class Synnax extends framer.Client {
103
104
  connectivityPollFrequency,
104
105
  secure,
105
106
  retry: breaker,
106
- } = props;
107
+ } = parsedParams;
107
108
  const transport = new Transport(
108
109
  new URL({ host, port: Number(port) }),
109
110
  breaker,
110
111
  secure,
111
112
  );
112
113
  transport.use(errorsMiddleware);
113
- let auth_: auth.Client | undefined;
114
- if (username != null && password != null) {
115
- auth_ = new auth.Client(transport.unary, { username, password });
116
- transport.use(auth_.middleware());
117
- }
118
114
  const chRetriever = new channel.CacheRetriever(
119
115
  new channel.ClusterRetriever(transport.unary),
120
116
  );
121
- const chCreator = new channel.Writer(transport.unary, chRetriever);
122
117
  super(transport.stream, transport.unary, chRetriever);
118
+ this.auth = new auth.Client(transport.unary, { username, password });
119
+ transport.use(this.auth.middleware());
120
+ const chCreator = new channel.Writer(transport.unary, chRetriever);
123
121
  this.createdAt = TimeStamp.now();
124
- this.props = props;
125
- this.auth = auth_;
122
+ this.params = parsedParams;
126
123
  this.transport = transport;
127
124
  this.channels = new channel.Client(this, chRetriever, transport.unary, chCreator);
128
125
  this.connectivity = new connection.Checker(
129
126
  transport.unary,
130
127
  connectivityPollFrequency,
131
128
  this.clientVersion,
132
- props.name,
129
+ parsedParams.name,
133
130
  );
134
131
  this.control = new control.Client(this);
135
132
  this.ontology = new ontology.Client(transport.unary, this);
@@ -147,15 +144,14 @@ export default class Synnax extends framer.Client {
147
144
  this.access = new access.Client(this.transport.unary);
148
145
  this.users = new user.Client(this.transport.unary);
149
146
  this.workspaces = new workspace.Client(this.transport.unary);
150
- const devices = new device.Client(this.transport.unary);
151
- const tasks = new task.Client(
147
+ this.tasks = new task.Client(
152
148
  this.transport.unary,
153
149
  this,
154
150
  this.ontology,
155
151
  this.ranges,
156
152
  );
157
- const racks = new rack.Client(this.transport.unary, tasks);
158
- this.hardware = new hardware.Client(tasks, racks, devices);
153
+ this.racks = new rack.Client(this.transport.unary, this.tasks);
154
+ this.devices = new device.Client(this.transport.unary);
159
155
  this.arcs = new arc.Client(this.transport.unary, this.transport.stream);
160
156
  }
161
157
 
@@ -164,6 +160,20 @@ export default class Synnax extends framer.Client {
164
160
  }
165
161
 
166
162
  close(): void {
167
- this.connectivity.stopChecking();
163
+ this.connectivity.stop();
168
164
  }
169
165
  }
166
+
167
+ export interface CheckConnectionParams
168
+ extends Pick<SynnaxParams, "host" | "port" | "secure" | "retry" | "name"> {}
169
+
170
+ export const checkConnection = async (params: CheckConnectionParams) =>
171
+ await newConnectionChecker(params).check();
172
+
173
+ export const newConnectionChecker = (params: CheckConnectionParams) => {
174
+ const { host, port, secure, name, retry } = params;
175
+ const retryConfig = breaker.breakerConfigZ.optional().parse(retry);
176
+ const url = new URL({ host, port: Number(port) });
177
+ const transport = new Transport(url, retryConfig, secure);
178
+ return new connection.Checker(transport.unary, undefined, __VERSION__, name);
179
+ };
@@ -7,7 +7,7 @@
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 { migrate, TimeSpan } from "@synnaxlabs/x";
12
12
  import { z } from "zod";
13
13
 
@@ -29,6 +29,7 @@ const responseZ = z.object({
29
29
  clusterKey: z.string(),
30
30
  nodeVersion: z.string().optional(),
31
31
  });
32
+ const requestZ = z.void();
32
33
 
33
34
  const DEFAULT: State = {
34
35
  clusterKey: "",
@@ -52,7 +53,6 @@ const createWarning = (
52
53
 
53
54
  /** Polls a synnax cluster for connectivity information. */
54
55
  export class Checker {
55
- private static readonly ENDPOINT = "/connectivity/check";
56
56
  static readonly DEFAULT: State = DEFAULT;
57
57
  private readonly _state: State;
58
58
  private readonly pollFrequency = TimeSpan.seconds(30);
@@ -81,11 +81,11 @@ export class Checker {
81
81
  this.clientVersion = clientVersion;
82
82
  this.name = name;
83
83
  void this.check();
84
- this.startChecking();
84
+ this.start();
85
85
  }
86
86
 
87
87
  /** Stops the connectivity client from polling the cluster for connectivity */
88
- stopChecking(): void {
88
+ stop(): void {
89
89
  if (this.interval != null) clearInterval(this.interval);
90
90
  }
91
91
 
@@ -96,13 +96,13 @@ export class Checker {
96
96
  async check(): Promise<State> {
97
97
  const prevStatus = this._state.status;
98
98
  try {
99
- const [res, err] = await this.client.send(
100
- Checker.ENDPOINT,
101
- {},
102
- z.object({}),
99
+ const res = await sendRequired(
100
+ this.client,
101
+ "/connectivity/check",
102
+ undefined,
103
+ requestZ,
103
104
  responseZ,
104
105
  );
105
- if (err != null) throw err;
106
106
  const nodeVersion = res.nodeVersion;
107
107
  const clientVersion = this.clientVersion;
108
108
  const warned = this.versionWarned;
@@ -156,7 +156,7 @@ export class Checker {
156
156
  this.onChangeHandlers.push(callback);
157
157
  }
158
158
 
159
- private startChecking(): void {
159
+ private start(): void {
160
160
  this.interval = setInterval(() => {
161
161
  void this.check();
162
162
  }, this.pollFrequency.milliseconds);
@@ -13,18 +13,18 @@ import { z } from "zod";
13
13
 
14
14
  import { auth } from "@/auth";
15
15
  import { connection } from "@/connection";
16
- import { TEST_CLIENT_PROPS } from "@/testutil/client";
16
+ import { TEST_CLIENT_PARAMS } from "@/testutil/client";
17
17
  import { Transport } from "@/transport";
18
18
 
19
19
  describe("connectivity", () => {
20
20
  it("should connect to the server", async () => {
21
21
  const transport = new Transport(
22
22
  new URL({
23
- host: TEST_CLIENT_PROPS.host,
24
- port: Number(TEST_CLIENT_PROPS.port),
23
+ host: TEST_CLIENT_PARAMS.host,
24
+ port: Number(TEST_CLIENT_PARAMS.port),
25
25
  }),
26
26
  );
27
- const client = new auth.Client(transport.unary, TEST_CLIENT_PROPS);
27
+ const client = new auth.Client(transport.unary, TEST_CLIENT_PARAMS);
28
28
  transport.use(client.middleware());
29
29
  const connectivity = new connection.Checker(
30
30
  transport.unary,
@@ -39,11 +39,11 @@ describe("connectivity", () => {
39
39
  it("should pull the server and client versions", async () => {
40
40
  const transport = new Transport(
41
41
  new URL({
42
- host: TEST_CLIENT_PROPS.host,
43
- port: Number(TEST_CLIENT_PROPS.port),
42
+ host: TEST_CLIENT_PARAMS.host,
43
+ port: Number(TEST_CLIENT_PARAMS.port),
44
44
  }),
45
45
  );
46
- const client = new auth.Client(transport.unary, TEST_CLIENT_PROPS);
46
+ const client = new auth.Client(transport.unary, TEST_CLIENT_PARAMS);
47
47
  transport.use(client.middleware());
48
48
  const connectivity = new connection.Checker(
49
49
  transport.unary,
@@ -57,11 +57,11 @@ describe("connectivity", () => {
57
57
  it("should adjust state if the server is too old", async () => {
58
58
  const transport = new Transport(
59
59
  new URL({
60
- host: TEST_CLIENT_PROPS.host,
61
- port: Number(TEST_CLIENT_PROPS.port),
60
+ host: TEST_CLIENT_PARAMS.host,
61
+ port: Number(TEST_CLIENT_PARAMS.port),
62
62
  }),
63
63
  );
64
- const client = new auth.Client(transport.unary, TEST_CLIENT_PROPS);
64
+ const client = new auth.Client(transport.unary, TEST_CLIENT_PARAMS);
65
65
  transport.use(client.middleware());
66
66
  const connectivity = new connection.Checker(
67
67
  transport.unary,
@@ -75,11 +75,11 @@ describe("connectivity", () => {
75
75
  it("should adjust state if the server is too new", async () => {
76
76
  const transport = new Transport(
77
77
  new URL({
78
- host: TEST_CLIENT_PROPS.host,
79
- port: Number(TEST_CLIENT_PROPS.port),
78
+ host: TEST_CLIENT_PARAMS.host,
79
+ port: Number(TEST_CLIENT_PARAMS.port),
80
80
  }),
81
81
  );
82
- const client = new auth.Client(transport.unary, TEST_CLIENT_PROPS);
82
+ const client = new auth.Client(transport.unary, TEST_CLIENT_PARAMS);
83
83
  transport.use(client.middleware());
84
84
  const connectivity = new connection.Checker(transport.unary, undefined, "0.0.0");
85
85
  const state = await connectivity.check();