@dxos/echo 0.9.1-main.c7dcc2e112 → 0.10.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 (278) hide show
  1. package/dist/lib/neutral/Annotation.mjs +22 -8
  2. package/dist/lib/neutral/Database.mjs +3 -3
  3. package/dist/lib/neutral/Entity.mjs +11 -11
  4. package/dist/lib/neutral/Err.mjs +7 -3
  5. package/dist/lib/neutral/Feed.mjs +15 -19
  6. package/dist/lib/neutral/Filter.mjs +13 -11
  7. package/dist/lib/neutral/Format.mjs +3 -7
  8. package/dist/lib/neutral/JsonSchema.mjs +7 -7
  9. package/dist/lib/neutral/Migration.mjs +11 -11
  10. package/dist/lib/neutral/Obj.mjs +15 -13
  11. package/dist/lib/neutral/Order.mjs +1 -1
  12. package/dist/lib/neutral/Query.mjs +17 -17
  13. package/dist/lib/neutral/QueryResult.mjs +1 -1
  14. package/dist/lib/neutral/Ref.mjs +6 -6
  15. package/dist/lib/neutral/Registry.mjs +1 -1
  16. package/dist/lib/neutral/Relation.mjs +12 -12
  17. package/dist/lib/neutral/Tag.mjs +15 -15
  18. package/dist/lib/neutral/Text.mjs +25 -0
  19. package/dist/lib/neutral/Text.mjs.map +7 -0
  20. package/dist/lib/neutral/Type.mjs +10 -10
  21. package/dist/lib/neutral/chunk-2HKGAQM4.mjs +1394 -0
  22. package/dist/lib/neutral/chunk-2HKGAQM4.mjs.map +7 -0
  23. package/dist/lib/neutral/{chunk-O6BH7EPN.mjs → chunk-2ZHECXUR.mjs} +5 -4
  24. package/dist/lib/neutral/chunk-2ZHECXUR.mjs.map +7 -0
  25. package/dist/lib/neutral/{chunk-AFANXSGV.mjs → chunk-3KTE2Y7Q.mjs} +20 -20
  26. package/dist/lib/neutral/chunk-3KTE2Y7Q.mjs.map +7 -0
  27. package/dist/lib/neutral/{chunk-WZ32BF53.mjs → chunk-3TN2VMI4.mjs} +49 -37
  28. package/dist/lib/neutral/chunk-3TN2VMI4.mjs.map +7 -0
  29. package/dist/lib/neutral/{chunk-E7IOTOZD.mjs → chunk-42L4HJ3Z.mjs} +11 -15
  30. package/dist/lib/neutral/chunk-42L4HJ3Z.mjs.map +7 -0
  31. package/dist/lib/neutral/{chunk-K3S7D5PR.mjs → chunk-4A2NWKRJ.mjs} +5 -5
  32. package/dist/lib/neutral/chunk-ACFAOTKJ.mjs +1531 -0
  33. package/dist/lib/neutral/chunk-ACFAOTKJ.mjs.map +7 -0
  34. package/dist/lib/neutral/{chunk-Z3WMRRAZ.mjs → chunk-BEFR7LEA.mjs} +11 -9
  35. package/dist/lib/neutral/{chunk-Z3WMRRAZ.mjs.map → chunk-BEFR7LEA.mjs.map} +1 -1
  36. package/dist/lib/neutral/{chunk-6M2Z6WBH.mjs → chunk-BXIKXT7Q.mjs} +2 -6
  37. package/dist/lib/neutral/{chunk-6M2Z6WBH.mjs.map → chunk-BXIKXT7Q.mjs.map} +1 -1
  38. package/dist/lib/neutral/{chunk-3JXQUMUI.mjs → chunk-C57XMMBY.mjs} +21 -20
  39. package/dist/lib/neutral/chunk-C57XMMBY.mjs.map +7 -0
  40. package/dist/lib/neutral/{chunk-JZ6LFQU5.mjs → chunk-C62JYFAD.mjs} +82 -8
  41. package/dist/lib/neutral/chunk-C62JYFAD.mjs.map +7 -0
  42. package/dist/lib/neutral/{chunk-TF5BSLF6.mjs → chunk-FSGX2R62.mjs} +3 -3
  43. package/dist/lib/neutral/chunk-FSGX2R62.mjs.map +7 -0
  44. package/dist/lib/neutral/{chunk-6CEISA3V.mjs → chunk-HBXT3GSB.mjs} +29 -15
  45. package/dist/lib/neutral/chunk-HBXT3GSB.mjs.map +7 -0
  46. package/dist/lib/neutral/{chunk-WU3GIANS.mjs → chunk-IQR45HAD.mjs} +1 -1
  47. package/dist/lib/neutral/chunk-IQR45HAD.mjs.map +7 -0
  48. package/dist/lib/neutral/{chunk-TFEWTY5A.mjs → chunk-KCBW4BR6.mjs} +4 -8
  49. package/dist/lib/neutral/chunk-KCBW4BR6.mjs.map +7 -0
  50. package/dist/lib/neutral/{chunk-4TPQZPLY.mjs → chunk-L3E5IXAE.mjs} +198 -30
  51. package/dist/lib/neutral/chunk-L3E5IXAE.mjs.map +7 -0
  52. package/dist/lib/neutral/{chunk-EVK6XBXO.mjs → chunk-PRC4FEKJ.mjs} +2 -2
  53. package/dist/lib/neutral/chunk-PRC4FEKJ.mjs.map +7 -0
  54. package/dist/lib/neutral/{chunk-NOL7O3BL.mjs → chunk-SCT6FFCR.mjs} +9 -4
  55. package/dist/lib/neutral/chunk-SCT6FFCR.mjs.map +7 -0
  56. package/dist/lib/neutral/{chunk-BBFJWWAV.mjs → chunk-TOCVVFK2.mjs} +26 -2
  57. package/dist/lib/neutral/chunk-TOCVVFK2.mjs.map +7 -0
  58. package/dist/lib/neutral/{chunk-A53NTBHE.mjs → chunk-U53LSQQA.mjs} +3 -3
  59. package/dist/lib/neutral/{chunk-A53NTBHE.mjs.map → chunk-U53LSQQA.mjs.map} +1 -1
  60. package/dist/lib/neutral/{chunk-PLDMJJJC.mjs → chunk-UGFFV2FR.mjs} +2 -2
  61. package/dist/lib/neutral/{chunk-35INCYOE.mjs → chunk-UWLP4RFM.mjs} +1 -1
  62. package/dist/lib/neutral/chunk-UWLP4RFM.mjs.map +7 -0
  63. package/dist/lib/neutral/{chunk-SZ5WB6HF.mjs → chunk-UZHCQ6NI.mjs} +13 -11
  64. package/dist/lib/neutral/{chunk-SZ5WB6HF.mjs.map → chunk-UZHCQ6NI.mjs.map} +3 -3
  65. package/dist/lib/neutral/{chunk-L3OBWBV2.mjs → chunk-WKIT2YY5.mjs} +2 -2
  66. package/dist/lib/neutral/{chunk-HGWQR5KH.mjs → chunk-XB6ISRAO.mjs} +27 -15
  67. package/dist/lib/neutral/chunk-XB6ISRAO.mjs.map +7 -0
  68. package/dist/lib/neutral/chunk-XXL42WCB.mjs +84 -0
  69. package/dist/lib/neutral/chunk-XXL42WCB.mjs.map +7 -0
  70. package/dist/lib/neutral/{chunk-5GAIWRQB.mjs → chunk-XXWK4J53.mjs} +27 -42
  71. package/dist/lib/neutral/chunk-XXWK4J53.mjs.map +7 -0
  72. package/dist/lib/neutral/index.mjs +30 -26
  73. package/dist/lib/neutral/internal/index.mjs +91 -69
  74. package/dist/lib/neutral/meta.json +1 -1
  75. package/dist/lib/neutral/testing/index.mjs +79 -60
  76. package/dist/lib/neutral/testing/index.mjs.map +3 -3
  77. package/dist/types/src/Annotation.d.ts +35 -1
  78. package/dist/types/src/Annotation.d.ts.map +1 -1
  79. package/dist/types/src/Collection.d.ts +9 -8
  80. package/dist/types/src/Collection.d.ts.map +1 -1
  81. package/dist/types/src/Database.d.ts +16 -0
  82. package/dist/types/src/Database.d.ts.map +1 -1
  83. package/dist/types/src/Dataset.d.ts +1 -16
  84. package/dist/types/src/Dataset.d.ts.map +1 -1
  85. package/dist/types/src/Err.d.ts +67 -0
  86. package/dist/types/src/Err.d.ts.map +1 -1
  87. package/dist/types/src/Feed.d.ts +56 -80
  88. package/dist/types/src/Feed.d.ts.map +1 -1
  89. package/dist/types/src/Filter.d.ts +9 -1
  90. package/dist/types/src/Filter.d.ts.map +1 -1
  91. package/dist/types/src/Hypergraph.d.ts +2 -9
  92. package/dist/types/src/Hypergraph.d.ts.map +1 -1
  93. package/dist/types/src/Migration.d.ts +1 -1
  94. package/dist/types/src/Migration.d.ts.map +1 -1
  95. package/dist/types/src/Obj.d.ts +31 -6
  96. package/dist/types/src/Obj.d.ts.map +1 -1
  97. package/dist/types/src/Order.d.ts +1 -1
  98. package/dist/types/src/Order.d.ts.map +1 -1
  99. package/dist/types/src/Query.d.ts +1 -1
  100. package/dist/types/src/Query.d.ts.map +1 -1
  101. package/dist/types/src/QueryResult.d.ts +2 -2
  102. package/dist/types/src/QueryResult.d.ts.map +1 -1
  103. package/dist/types/src/Ref.d.ts +8 -4
  104. package/dist/types/src/Ref.d.ts.map +1 -1
  105. package/dist/types/src/Registry.d.ts +1 -1
  106. package/dist/types/src/Relation.d.ts.map +1 -1
  107. package/dist/types/src/Tag.d.ts +11 -13
  108. package/dist/types/src/Tag.d.ts.map +1 -1
  109. package/dist/types/src/Text.d.ts +41 -0
  110. package/dist/types/src/Text.d.ts.map +1 -0
  111. package/dist/types/src/Text.test.d.ts +2 -0
  112. package/dist/types/src/Text.test.d.ts.map +1 -0
  113. package/dist/types/src/Type.d.ts +33 -20
  114. package/dist/types/src/Type.d.ts.map +1 -1
  115. package/dist/types/src/View.d.ts +8 -9
  116. package/dist/types/src/View.d.ts.map +1 -1
  117. package/dist/types/src/index.d.ts +1 -0
  118. package/dist/types/src/index.d.ts.map +1 -1
  119. package/dist/types/src/internal/Annotation/annotations.d.ts +9 -0
  120. package/dist/types/src/internal/Annotation/annotations.d.ts.map +1 -1
  121. package/dist/types/src/internal/Annotation/atoms.d.ts +14 -0
  122. package/dist/types/src/internal/Annotation/atoms.d.ts.map +1 -0
  123. package/dist/types/src/internal/Annotation/entity-dictionary.d.ts +13 -0
  124. package/dist/types/src/internal/Annotation/entity-dictionary.d.ts.map +1 -1
  125. package/dist/types/src/internal/Entity/api.d.ts.map +1 -1
  126. package/dist/types/src/internal/Entity/guard.d.ts.map +1 -1
  127. package/dist/types/src/internal/Entity/model.d.ts +1 -1
  128. package/dist/types/src/internal/Entity/model.d.ts.map +1 -1
  129. package/dist/types/src/internal/Entity/object.d.ts +5 -1
  130. package/dist/types/src/internal/Entity/object.d.ts.map +1 -1
  131. package/dist/types/src/internal/Entity/relation.d.ts +7 -0
  132. package/dist/types/src/internal/Entity/relation.d.ts.map +1 -1
  133. package/dist/types/src/internal/Entity/type-uri.d.ts.map +1 -1
  134. package/dist/types/src/internal/Filter/index.d.ts +2 -0
  135. package/dist/types/src/internal/Filter/index.d.ts.map +1 -0
  136. package/dist/types/src/internal/Filter/match.d.ts +22 -0
  137. package/dist/types/src/internal/Filter/match.d.ts.map +1 -0
  138. package/dist/types/src/internal/Format/format.d.ts +0 -2
  139. package/dist/types/src/internal/Format/format.d.ts.map +1 -1
  140. package/dist/types/src/internal/Format/index.d.ts +2 -2
  141. package/dist/types/src/internal/Format/index.d.ts.map +1 -1
  142. package/dist/types/src/internal/Format/string.d.ts +0 -8
  143. package/dist/types/src/internal/Format/string.d.ts.map +1 -1
  144. package/dist/types/src/internal/Format/types.d.ts +2 -1
  145. package/dist/types/src/internal/Format/types.d.ts.map +1 -1
  146. package/dist/types/src/internal/Obj/atoms.d.ts +7 -3
  147. package/dist/types/src/internal/Obj/atoms.d.ts.map +1 -1
  148. package/dist/types/src/internal/Obj/clone.d.ts +2 -1
  149. package/dist/types/src/internal/Obj/clone.d.ts.map +1 -1
  150. package/dist/types/src/internal/Obj/create-object.d.ts +3 -4
  151. package/dist/types/src/internal/Obj/create-object.d.ts.map +1 -1
  152. package/dist/types/src/internal/Obj/json-serializer.d.ts.map +1 -1
  153. package/dist/types/src/internal/Query/index.d.ts +1 -0
  154. package/dist/types/src/internal/Query/index.d.ts.map +1 -1
  155. package/dist/types/src/internal/Query/query-result-effect.d.ts +9 -0
  156. package/dist/types/src/internal/Query/query-result-effect.d.ts.map +1 -0
  157. package/dist/types/src/internal/Ref/atoms.d.ts +1 -2
  158. package/dist/types/src/internal/Ref/atoms.d.ts.map +1 -1
  159. package/dist/types/src/internal/Ref/ref.d.ts +13 -6
  160. package/dist/types/src/internal/Ref/ref.d.ts.map +1 -1
  161. package/dist/types/src/internal/common/atom-snapshot.d.ts +8 -0
  162. package/dist/types/src/internal/common/atom-snapshot.d.ts.map +1 -0
  163. package/dist/types/src/internal/common/proxy/make-object.d.ts.map +1 -1
  164. package/dist/types/src/internal/common/proxy/proxy-types.d.ts +12 -0
  165. package/dist/types/src/internal/common/proxy/proxy-types.d.ts.map +1 -1
  166. package/dist/types/src/internal/common/proxy/proxy-utils.d.ts +25 -0
  167. package/dist/types/src/internal/common/proxy/proxy-utils.d.ts.map +1 -1
  168. package/dist/types/src/internal/common/proxy/schema-validator.d.ts +5 -0
  169. package/dist/types/src/internal/common/proxy/schema-validator.d.ts.map +1 -1
  170. package/dist/types/src/internal/common/proxy/typed-handler.d.ts +17 -0
  171. package/dist/types/src/internal/common/proxy/typed-handler.d.ts.map +1 -1
  172. package/dist/types/src/internal/common/types/entity.d.ts +4 -4
  173. package/dist/types/src/internal/common/types/entity.d.ts.map +1 -1
  174. package/dist/types/src/internal/common/types/meta.d.ts +2 -4
  175. package/dist/types/src/internal/common/types/meta.d.ts.map +1 -1
  176. package/dist/types/src/internal/index.d.ts +1 -0
  177. package/dist/types/src/internal/index.d.ts.map +1 -1
  178. package/dist/types/src/testing/test-schema.d.ts +42 -171
  179. package/dist/types/src/testing/test-schema.d.ts.map +1 -1
  180. package/dist/types/tsconfig.tsbuildinfo +1 -1
  181. package/package.json +20 -13
  182. package/src/Annotation.test.ts +257 -10
  183. package/src/Annotation.ts +55 -6
  184. package/src/Collection.ts +10 -12
  185. package/src/Database.ts +22 -20
  186. package/src/Err.ts +22 -0
  187. package/src/Feed.ts +114 -141
  188. package/src/Filter.test.ts +38 -0
  189. package/src/Filter.ts +17 -3
  190. package/src/Hypergraph.ts +2 -11
  191. package/src/Migration.ts +1 -1
  192. package/src/Obj.test.ts +92 -12
  193. package/src/Obj.ts +36 -5
  194. package/src/Order.ts +3 -3
  195. package/src/Query.test.ts +2 -2
  196. package/src/Query.ts +16 -16
  197. package/src/QueryResult.ts +2 -2
  198. package/src/Ref.ts +12 -4
  199. package/src/Registry.ts +1 -1
  200. package/src/Relation.ts +1 -1
  201. package/src/Tag.ts +12 -16
  202. package/src/Text.test.ts +186 -0
  203. package/src/Text.ts +116 -0
  204. package/src/Type.test.ts +52 -24
  205. package/src/Type.ts +69 -31
  206. package/src/View.ts +22 -30
  207. package/src/index.ts +1 -1
  208. package/src/internal/Annotation/annotations.ts +15 -2
  209. package/src/internal/Annotation/atoms.ts +107 -0
  210. package/src/internal/Annotation/entity-dictionary.ts +40 -37
  211. package/src/internal/Entity/api.ts +1 -1
  212. package/src/internal/Entity/guard.ts +2 -1
  213. package/src/internal/Entity/model.ts +2 -2
  214. package/src/internal/Entity/object.ts +14 -1
  215. package/src/internal/Entity/relation.ts +19 -0
  216. package/src/internal/Entity/type-uri.ts +3 -1
  217. package/src/internal/Filter/index.ts +5 -0
  218. package/src/internal/Filter/match.ts +284 -0
  219. package/src/internal/Format/format.ts +0 -2
  220. package/src/internal/Format/index.ts +2 -2
  221. package/src/internal/Format/string.ts +0 -12
  222. package/src/internal/Format/types.ts +4 -2
  223. package/src/internal/JsonSchema/json-schema.test.ts +82 -54
  224. package/src/internal/Obj/atoms.ts +31 -18
  225. package/src/internal/Obj/clone.ts +61 -20
  226. package/src/internal/Obj/create-object.test.ts +3 -3
  227. package/src/internal/Obj/create-object.ts +3 -4
  228. package/src/internal/Obj/inspect.ts +1 -1
  229. package/src/internal/Obj/json-serializer.test.ts +7 -5
  230. package/src/internal/Obj/json-serializer.ts +1 -1
  231. package/src/internal/Obj/schema-validator.test.ts +53 -0
  232. package/src/internal/Obj/set-value.test.ts +29 -19
  233. package/src/internal/Query/index.ts +1 -0
  234. package/src/internal/Query/query-result-effect.ts +35 -0
  235. package/src/internal/Ref/atoms.ts +23 -3
  236. package/src/internal/Ref/ref.test.ts +8 -8
  237. package/src/internal/Ref/ref.ts +26 -20
  238. package/src/internal/common/atom-snapshot.ts +19 -0
  239. package/src/internal/common/proxy/make-object.ts +1 -1
  240. package/src/internal/common/proxy/proxy-types.ts +20 -0
  241. package/src/internal/common/proxy/proxy-utils.ts +37 -1
  242. package/src/internal/common/proxy/reactive-array.ts +3 -3
  243. package/src/internal/common/proxy/reactive.test.ts +1 -1
  244. package/src/internal/common/proxy/schema-validator.ts +181 -8
  245. package/src/internal/common/proxy/typed-handler.ts +254 -76
  246. package/src/internal/common/types/entity.ts +15 -7
  247. package/src/internal/index.ts +1 -0
  248. package/src/testing/test-schema.ts +75 -90
  249. package/dist/lib/neutral/chunk-35INCYOE.mjs.map +0 -7
  250. package/dist/lib/neutral/chunk-3JXQUMUI.mjs.map +0 -7
  251. package/dist/lib/neutral/chunk-4TPQZPLY.mjs.map +0 -7
  252. package/dist/lib/neutral/chunk-5GAIWRQB.mjs.map +0 -7
  253. package/dist/lib/neutral/chunk-6CEISA3V.mjs.map +0 -7
  254. package/dist/lib/neutral/chunk-AFANXSGV.mjs.map +0 -7
  255. package/dist/lib/neutral/chunk-BBFJWWAV.mjs.map +0 -7
  256. package/dist/lib/neutral/chunk-E7IOTOZD.mjs.map +0 -7
  257. package/dist/lib/neutral/chunk-EVK6XBXO.mjs.map +0 -7
  258. package/dist/lib/neutral/chunk-HGWQR5KH.mjs.map +0 -7
  259. package/dist/lib/neutral/chunk-JZ6LFQU5.mjs.map +0 -7
  260. package/dist/lib/neutral/chunk-NOL7O3BL.mjs.map +0 -7
  261. package/dist/lib/neutral/chunk-O6BH7EPN.mjs.map +0 -7
  262. package/dist/lib/neutral/chunk-TF5BSLF6.mjs.map +0 -7
  263. package/dist/lib/neutral/chunk-TFEWTY5A.mjs.map +0 -7
  264. package/dist/lib/neutral/chunk-VQYL2OQF.mjs +0 -2353
  265. package/dist/lib/neutral/chunk-VQYL2OQF.mjs.map +0 -7
  266. package/dist/lib/neutral/chunk-WU3GIANS.mjs.map +0 -7
  267. package/dist/lib/neutral/chunk-WZ32BF53.mjs.map +0 -7
  268. package/dist/lib/neutral/chunk-X3NUUON4.mjs +0 -184
  269. package/dist/lib/neutral/chunk-X3NUUON4.mjs.map +0 -7
  270. package/dist/types/src/exemplars.test.d.ts +0 -2
  271. package/dist/types/src/exemplars.test.d.ts.map +0 -1
  272. package/dist/types/src/hierarchy.test.d.ts +0 -2
  273. package/dist/types/src/hierarchy.test.d.ts.map +0 -1
  274. package/src/exemplars.test.ts +0 -21
  275. package/src/hierarchy.test.ts +0 -33
  276. /package/dist/lib/neutral/{chunk-K3S7D5PR.mjs.map → chunk-4A2NWKRJ.mjs.map} +0 -0
  277. /package/dist/lib/neutral/{chunk-PLDMJJJC.mjs.map → chunk-UGFFV2FR.mjs.map} +0 -0
  278. /package/dist/lib/neutral/{chunk-L3OBWBV2.mjs.map → chunk-WKIT2YY5.mjs.map} +0 -0
@@ -32,18 +32,6 @@ export const Formula = Schema.String.pipe(FormatAnnotation.set(TypeFormat.Formul
32
32
  // TODO(burdon): Implement.
33
33
  export const Hostname = Schema.String.pipe(FormatAnnotation.set(TypeFormat.Hostname));
34
34
 
35
- /**
36
- *
37
- */
38
- // TODO(burdon): Implement.
39
- export const JSON = Schema.String.pipe(FormatAnnotation.set(TypeFormat.JSON));
40
-
41
- /**
42
- *
43
- */
44
- // TODO(burdon): Implement.
45
- export const Markdown = Schema.String.pipe(FormatAnnotation.set(TypeFormat.Markdown));
46
-
47
35
  /**
48
36
  * Regex
49
37
  * https://json-schema.org/understanding-json-schema/reference/regular_expressions
@@ -7,7 +7,7 @@ import type * as JSONSchema from 'effect/JSONSchema';
7
7
  import * as Option from 'effect/Option';
8
8
  import * as SchemaAST from 'effect/SchemaAST';
9
9
 
10
- import { createAnnotationHelper } from '../Annotation/util';
10
+ import { createAnnotationHelper } from '../Annotation';
11
11
  import { type JsonSchemaType } from '../JsonSchema';
12
12
 
13
13
  // TODO(burdon): Rename PropertyType.
@@ -79,10 +79,11 @@ export enum TypeFormat {
79
79
  DID = 'did', // Users, etc.
80
80
  DXN = 'dxn',
81
81
  Email = 'email',
82
- Formula = 'formula', // Spreadsheet formula.
82
+ Formula = 'formula', // Spreadsheet (Excel, Google Sheets, etc.) formula.
83
83
  Hostname = 'hostname',
84
84
  JSON = 'json',
85
85
  Markdown = 'markdown',
86
+ Password = 'password',
86
87
  Regex = 'regex',
87
88
  SingleSelect = 'single-select',
88
89
  Text = 'text',
@@ -149,6 +150,7 @@ export const formatToType: Record<TypeFormat, TypeEnum> = {
149
150
  [TypeFormat.Hostname]: TypeEnum.String,
150
151
  [TypeFormat.JSON]: TypeEnum.String,
151
152
  [TypeFormat.Markdown]: TypeEnum.String,
153
+ [TypeFormat.Password]: TypeEnum.String,
152
154
  [TypeFormat.Regex]: TypeEnum.String,
153
155
  [TypeFormat.URL]: TypeEnum.String,
154
156
  [TypeFormat.UUID]: TypeEnum.String,
@@ -33,9 +33,7 @@ const EXAMPLE_NAMESPACE = '@example';
33
33
 
34
34
  describe('effect-to-json', () => {
35
35
  test('type annotation', () => {
36
- const Test = Schema.Struct({ name: Schema.String }).pipe(
37
- Type.makeObject(DXN.make('com.example.type.test', '0.1.0')),
38
- );
36
+ const Test = Type.makeObject(DXN.make('com.example.type.test', '0.1.0'))(Schema.Struct({ name: Schema.String }));
39
37
  const jsonSchema = toJsonSchema(Test);
40
38
  expect(jsonSchema.$id).toEqual('dxn:com.example.type.test:0.1.0');
41
39
  expect(jsonSchema.version).toEqual('0.1.0');
@@ -43,20 +41,26 @@ describe('effect-to-json', () => {
43
41
 
44
42
  test('property meta annotation', () => {
45
43
  const meta = { maxLength: 0 };
46
- const Test = Schema.Struct({
47
- name: Schema.String.pipe(PropertyMeta(EXAMPLE_NAMESPACE, meta)),
48
- }).pipe(Type.makeObject(DXN.make('com.example.type.test', '0.1.0')));
44
+ const Test = Type.makeObject(DXN.make('com.example.type.test', '0.1.0'))(
45
+ Schema.Struct({
46
+ name: Schema.String.pipe(PropertyMeta(EXAMPLE_NAMESPACE, meta)),
47
+ }),
48
+ );
49
49
  const jsonSchema = toJsonSchema(Test);
50
50
  expect(getNormalizedEchoAnnotations(jsonSchema.properties!.name!)!.meta![EXAMPLE_NAMESPACE]).to.deep.eq(meta);
51
51
  });
52
52
 
53
53
  test('reference annotation', () => {
54
- const Nested = Schema.Struct({
55
- name: Schema.String,
56
- }).pipe(Type.makeObject(DXN.make('com.example.type.testNested', '0.1.0')));
57
- const Test = Schema.Struct({
58
- name: Ref(Nested),
59
- }).pipe(Type.makeObject(DXN.make('com.example.type.test', '0.1.0')));
54
+ const Nested = Type.makeObject(DXN.make('com.example.type.testNested', '0.1.0'))(
55
+ Schema.Struct({
56
+ name: Schema.String,
57
+ }),
58
+ );
59
+ const Test = Type.makeObject(DXN.make('com.example.type.test', '0.1.0'))(
60
+ Schema.Struct({
61
+ name: Ref(Nested),
62
+ }),
63
+ );
60
64
  const jsonSchema = toJsonSchema(Test);
61
65
  // log.info('schema', { jsonSchema });
62
66
  const nested = jsonSchema.properties!.name;
@@ -66,12 +70,16 @@ describe('effect-to-json', () => {
66
70
  // TODO(ZaymonFC): @dmaretskyi we still need to fix this.
67
71
  // TODO(dmaretskyi): Remove FieldLookupAnnotationId.
68
72
  test.skip('reference annotation with lookup property', () => {
69
- const Nested = Schema.Struct({
70
- name: Schema.String,
71
- }).pipe(Type.makeObject(DXN.make('com.example.type.testNested', '0.1.0')));
72
- const Test = Schema.Struct({
73
- name: Ref(Nested).annotations({ [FieldLookupAnnotationId]: 'name' }),
74
- }).pipe(Type.makeObject(DXN.make('com.example.type.test', '0.1.0')));
73
+ const Nested = Type.makeObject(DXN.make('com.example.type.testNested', '0.1.0'))(
74
+ Schema.Struct({
75
+ name: Schema.String,
76
+ }),
77
+ );
78
+ const Test = Type.makeObject(DXN.make('com.example.type.test', '0.1.0'))(
79
+ Schema.Struct({
80
+ name: Ref(Nested).annotations({ [FieldLookupAnnotationId]: 'name' }),
81
+ }),
82
+ );
75
83
  const jsonSchema = toJsonSchema(Test);
76
84
  const effectSchema = toEffectSchema(jsonSchema);
77
85
 
@@ -80,24 +88,32 @@ describe('effect-to-json', () => {
80
88
  });
81
89
 
82
90
  test('array of references', () => {
83
- const Nested = Schema.Struct({
84
- name: Schema.String,
85
- }).pipe(Type.makeObject(DXN.make('com.example.type.testNested', '0.1.0')));
86
- const Test = Schema.Struct({
87
- name: Schema.Array(Ref(Nested)),
88
- }).pipe(Type.makeObject(DXN.make('com.example.type.test', '0.1.0')));
91
+ const Nested = Type.makeObject(DXN.make('com.example.type.testNested', '0.1.0'))(
92
+ Schema.Struct({
93
+ name: Schema.String,
94
+ }),
95
+ );
96
+ const Test = Type.makeObject(DXN.make('com.example.type.test', '0.1.0'))(
97
+ Schema.Struct({
98
+ name: Schema.Array(Ref(Nested)),
99
+ }),
100
+ );
89
101
 
90
102
  const jsonSchema = toJsonSchema(Test);
91
103
  expectReferenceAnnotation((jsonSchema.properties!.name as any).items);
92
104
  });
93
105
 
94
106
  test('optional references', () => {
95
- const Nested = Schema.Struct({
96
- name: Schema.String,
97
- }).pipe(Type.makeObject(DXN.make('com.example.type.testNested', '0.1.0')));
98
- const Test = Schema.Struct({
99
- name: Schema.optional(Ref(Nested)),
100
- }).pipe(Type.makeObject(DXN.make('com.example.type.test', '0.1.0')));
107
+ const Nested = Type.makeObject(DXN.make('com.example.type.testNested', '0.1.0'))(
108
+ Schema.Struct({
109
+ name: Schema.String,
110
+ }),
111
+ );
112
+ const Test = Type.makeObject(DXN.make('com.example.type.test', '0.1.0'))(
113
+ Schema.Struct({
114
+ name: Schema.optional(Ref(Nested)),
115
+ }),
116
+ );
101
117
  const jsonSchema = toJsonSchema(Test);
102
118
  expectReferenceAnnotation(jsonSchema.properties!.name);
103
119
  });
@@ -110,12 +126,14 @@ describe('effect-to-json', () => {
110
126
  });
111
127
 
112
128
  test('annotations', () => {
113
- const TempSchema = Schema.Struct({
114
- name: Schema.String.annotations({ description: 'Person name', title: 'Name' }),
115
- email: Schema.String.pipe(FormatAnnotation.set(TypeFormat.Email)).annotations({
116
- description: 'Email address',
129
+ const TempSchema = Type.makeObject(DXN.make('com.example.type.person', '0.1.0'))(
130
+ Schema.Struct({
131
+ name: Schema.String.annotations({ description: 'Person name', title: 'Name' }),
132
+ email: Schema.String.pipe(FormatAnnotation.set(TypeFormat.Email)).annotations({
133
+ description: 'Email address',
134
+ }),
117
135
  }),
118
- }).pipe(Type.makeObject(DXN.make('com.example.type.person', '0.1.0')));
136
+ );
119
137
 
120
138
  const jsonSchema = toJsonSchema(TempSchema);
121
139
  expect(jsonSchema).to.deep.eq({
@@ -159,14 +177,18 @@ describe('effect-to-json', () => {
159
177
  });
160
178
 
161
179
  test('reference property by ref', () => {
162
- const Organization = Schema.Struct({
163
- field: Schema.String,
164
- }).pipe(Type.makeObject(DXN.make('com.example.type.organization', '0.1.0')));
180
+ const Organization = Type.makeObject(DXN.make('com.example.type.organization', '0.1.0'))(
181
+ Schema.Struct({
182
+ field: Schema.String,
183
+ }),
184
+ );
165
185
 
166
- const Contact = Schema.Struct({
167
- name: Schema.String,
168
- organization: Ref(Organization).annotations({ description: 'Contact organization' }),
169
- }).pipe(Type.makeObject(DXN.make('com.example.type.person', '0.1.0')));
186
+ const Contact = Type.makeObject(DXN.make('com.example.type.person', '0.1.0'))(
187
+ Schema.Struct({
188
+ name: Schema.String,
189
+ organization: Ref(Organization).annotations({ description: 'Contact organization' }),
190
+ }),
191
+ );
170
192
 
171
193
  // log.info('Contact', { ast: Contact.ast });
172
194
 
@@ -211,14 +233,18 @@ describe('effect-to-json', () => {
211
233
  });
212
234
 
213
235
  test('add reference property', () => {
214
- const Organization = Schema.Struct({
215
- field: Schema.String,
216
- }).pipe(Type.makeObject(DXN.make('com.example.type.organization', '0.1.0')));
236
+ const Organization = Type.makeObject(DXN.make('com.example.type.organization', '0.1.0'))(
237
+ Schema.Struct({
238
+ field: Schema.String,
239
+ }),
240
+ );
217
241
 
218
- const Contact = Schema.Struct({
219
- name: Schema.String,
220
- organization: Ref(Organization).annotations({ description: 'Contact organization' }),
221
- }).pipe(Type.makeObject(DXN.make('com.example.type.person', '0.1.0')));
242
+ const Contact = Type.makeObject(DXN.make('com.example.type.person', '0.1.0'))(
243
+ Schema.Struct({
244
+ name: Schema.String,
245
+ organization: Ref(Organization).annotations({ description: 'Contact organization' }),
246
+ }),
247
+ );
222
248
 
223
249
  const jsonSchema = toJsonSchema(Contact);
224
250
  setSchemaProperty(
@@ -418,9 +444,11 @@ describe('json-to-effect', () => {
418
444
 
419
445
  for (const partial of [false, true]) {
420
446
  test(`deserialized equals original ${partial ? 'with partial' : ''}`, () => {
421
- const Organization = Schema.Struct({
422
- field: Schema.String,
423
- }).pipe(Type.makeObject(DXN.make('com.example.type.organization', '0.1.0')));
447
+ const Organization = Type.makeObject(DXN.make('com.example.type.organization', '0.1.0'))(
448
+ Schema.Struct({
449
+ field: Schema.String,
450
+ }),
451
+ );
424
452
 
425
453
  const fields = {
426
454
  string: Schema.String,
@@ -436,8 +464,8 @@ describe('json-to-effect', () => {
436
464
  null: Schema.Null,
437
465
  } as const;
438
466
 
439
- const Test = (partial ? Schema.partial(Schema.Struct(fields)) : Schema.Struct(fields)).pipe(
440
- Type.makeObject(DXN.make('com.example.type.test', '0.1.0')),
467
+ const Test = Type.makeObject(DXN.make('com.example.type.test', '0.1.0'))(
468
+ partial ? Schema.partial(Schema.Struct(fields)) : Schema.Struct(fields),
441
469
  );
442
470
 
443
471
  const jsonSchema = toJsonSchema(Test);
@@ -15,10 +15,12 @@ import type * as Obj from '../../Obj';
15
15
  import type * as Ref from '../../Ref';
16
16
  import type * as Relation from '../../Relation';
17
17
  import { getLabel } from '../Annotation';
18
+ import { snapshotForComparison } from '../common/atom-snapshot';
18
19
  import { subscribe } from '../common/proxy/reactive';
19
- import { isEntity, getDatabase } from '../Entity';
20
+ import { getDatabase, isEntity } from '../Entity';
20
21
  import { RefTypeId } from '../Ref/ref';
21
22
  import { loadRefTarget } from '../Ref/utils';
23
+ import { isDeleted } from './deleted';
22
24
  import { getSnapshot } from './snapshot';
23
25
 
24
26
  const isRef = (obj: unknown): obj is Ref.Ref<any> =>
@@ -63,8 +65,9 @@ const refFamily = Atom.family(<T extends Obj.Unknown>(ref: Ref.Ref<T>): Atom.Ato
63
65
  const setupTargetSubscription = (target: T): Obj.Snapshot<T> => {
64
66
  unsubscribeTarget?.();
65
67
  unsubscribeTarget = subscribe(target, () => {
68
+ // Deleted objects resolve to undefined so callers don't need to inspect isDeleted.
66
69
  // getSnapshot adds SnapshotKindId brand at runtime; cast bridges static types.
67
- get.setSelf(getSnapshot(target) as unknown as Obj.Snapshot<T>);
70
+ get.setSelf(isDeleted(target) ? undefined : (getSnapshot(target) as unknown as Obj.Snapshot<T>));
68
71
  });
69
72
  return getSnapshot(target) as unknown as Obj.Snapshot<T>;
70
73
  };
@@ -77,19 +80,6 @@ const refFamily = Atom.family(<T extends Obj.Unknown>(ref: Ref.Ref<T>): Atom.Ato
77
80
  }).pipe(Atom.keepAlive);
78
81
  });
79
82
 
80
- /**
81
- * Snapshot a value to create a new reference for comparison and React dependency tracking.
82
- */
83
- const snapshotForComparison = <V>(value: V): V => {
84
- if (Array.isArray(value)) {
85
- return [...value] as V;
86
- }
87
- if (value !== null && typeof value === 'object') {
88
- return { ...value } as V;
89
- }
90
- return value;
91
- };
92
-
93
83
  /**
94
84
  * Atom family for ECHO object properties.
95
85
  * Uses nested families: outer keyed by object, inner keyed by property key.
@@ -150,6 +140,20 @@ const refWithReactiveFamily = Atom.family(<T extends Obj.Unknown>(ref: Ref.Ref<T
150
140
  );
151
141
  });
152
142
 
143
+ /**
144
+ * Atom family for a property of a ref's target object.
145
+ * Resolves the ref (reactively) then projects the property atom, so it fires when the ref resolves or when
146
+ * that property changes. Yields `undefined` while the target is unresolved.
147
+ */
148
+ const refPropertyFamily = Atom.family(<T extends Obj.Unknown>(ref: Ref.Ref<T>) =>
149
+ Atom.family(<K extends keyof T>(key: K): Atom.Atom<T[K] | undefined> => {
150
+ return Atom.make<T[K] | undefined>((get) => {
151
+ const target = get(refWithReactiveFamily(ref));
152
+ return target ? get(propertyFamily(target)(key)) : undefined;
153
+ }).pipe(Atom.keepAlive);
154
+ }),
155
+ );
156
+
153
157
  /**
154
158
  * Atom family for any ECHO entity (obj or relation) — returns a snapshot.
155
159
  */
@@ -201,10 +205,19 @@ export const makeAtom: {
201
205
  };
202
206
 
203
207
  /**
204
- * Create a read-only atom for a specific property of a reactive object.
205
- * Only fires updates when the property value actually changes.
208
+ * Create a read-only atom for a specific property of a reactive object, or of a ref's target.
209
+ * Only fires updates when the property value actually changes. Given a ref, the atom resolves the target
210
+ * first and yields `undefined` until it loads.
206
211
  */
207
- export const makeProperty = <T extends Obj.Unknown, K extends keyof T>(obj: T, key: K): Atom.Atom<T[K]> => {
212
+ export const makeProperty: {
213
+ <T extends Obj.Unknown, K extends keyof T>(obj: T, key: K): Atom.Atom<T[K]>;
214
+ <T extends Obj.Unknown, K extends keyof T>(ref: Ref.Ref<T>, key: K): Atom.Atom<T[K] | undefined>;
215
+ } = (objOrRef: Obj.Unknown | Ref.Ref<any>, key: any): Atom.Atom<any> => {
216
+ if (isRef(objOrRef)) {
217
+ return refPropertyFamily(objOrRef as Ref.Ref<any>)(key);
218
+ }
219
+
220
+ const obj = objOrRef as Obj.Unknown;
208
221
  assertArgument(isEntity(obj), 'obj', 'Object must be a reactive object');
209
222
  return propertyFamily(obj)(key);
210
223
  };
@@ -3,20 +3,42 @@
3
3
  //
4
4
 
5
5
  import { invariant } from '@dxos/invariant';
6
- import { deepMapValues } from '@dxos/util';
6
+ import { assumeType, deepMapValues } from '@dxos/util';
7
7
 
8
8
  import type * as Obj from '../../Obj';
9
9
  import { makeObject } from '../common/proxy';
10
- import { getSchema, getStaticTypeSchema, getType } from '../common/types';
10
+ import { ParentId, getSchema, getStaticTypeSchema, getType } from '../common/types';
11
11
  import { getMeta } from '../common/types/meta';
12
+ import { type InternalObjectProps } from '../Entity/model';
12
13
  import { Ref } from '../Ref';
13
14
 
14
15
  /**
15
16
  * Clones an object or relation.
16
- * This does not clone referenced objects, only the properties in the object.
17
+ * By default this does not clone referenced objects, only the properties in the object; pass `opts.deep`
18
+ * to recurse into refs (see {@link Obj.CloneOptions}).
17
19
  * @returns A new object with the same schema and properties.
18
20
  */
19
- export const clone = <T extends Obj.Any>(obj: T, opts?: Obj.CloneOptions): T => {
21
+ export const clone = <T extends Obj.Any>(obj: T, opts?: Obj.CloneOptions): T =>
22
+ cloneInner(obj, opts, obj, new Map(), new Set());
23
+
24
+ /**
25
+ * Recursive clone worker threaded with two cycle-guard structures:
26
+ * - `cache`: id → already-finished clone; reused when the same object appears more than once in a DAG.
27
+ * - `inProgress`: ids of objects whose clone is currently being computed; a ref back into this set means
28
+ * a true cycle — the original ref is kept rather than recursing forever.
29
+ */
30
+ const cloneInner = <T extends Obj.Any>(
31
+ obj: T,
32
+ opts: Obj.CloneOptions | undefined,
33
+ root: Obj.Any,
34
+ cache: Map<string, Obj.Any>,
35
+ inProgress: Set<string>,
36
+ ): T => {
37
+ if (cache.has(obj.id)) {
38
+ return cache.get(obj.id) as T;
39
+ }
40
+ inProgress.add(obj.id);
41
+
20
42
  const { id, ...data } = obj;
21
43
  // Prefer cloning through the type entity so the cloned instance preserves
22
44
  // `Obj.getType` identity. Falls back to the raw schema for older instances
@@ -24,30 +46,49 @@ export const clone = <T extends Obj.Any>(obj: T, opts?: Obj.CloneOptions): T =>
24
46
  const typeEntity = getType(obj);
25
47
  const schema = typeEntity != null ? getStaticTypeSchema(typeEntity) : getSchema(obj);
26
48
  invariant(schema != null, 'Object should have a type or schema');
27
- const props: any = deepMapValues(data, (value, recurse) => {
49
+
50
+ const mapRef = (value: unknown, recurse: (value: unknown) => unknown): unknown => {
28
51
  if (Ref.isRef(value)) {
29
- if (opts?.deep) {
30
- // TODO(dmaretskyi): Will break on circular references.
31
- return Ref.make(clone(value.target!, opts));
52
+ const target = value.target;
53
+ // A ref back to an in-progress object means a cycle — keep the original ref to break it.
54
+ if (
55
+ target != null &&
56
+ !inProgress.has(target.id) &&
57
+ (opts?.deep === 'all' || (opts?.deep === 'parent' && isChildOf(target, root)))
58
+ ) {
59
+ return Ref.make(cloneInner(target, opts, root, cache, inProgress));
32
60
  }
33
61
  return value;
34
62
  }
35
63
  return recurse(value);
36
- });
64
+ };
37
65
 
66
+ const props: any = deepMapValues(data, mapRef);
38
67
  if (opts?.retainId) {
39
68
  props.id = id;
40
69
  }
41
- const meta = deepMapValues(getMeta(obj), (value, recurse) => {
42
- if (Ref.isRef(value)) {
43
- if (opts?.deep) {
44
- // TODO(dmaretskyi): Will break on circular references.
45
- return Ref.make(clone(value.target!, opts));
46
- }
47
- return value;
48
- }
49
- return recurse(value);
50
- });
70
+ const meta = deepMapValues(getMeta(obj), mapRef);
71
+ const result = makeObject(schema, props, meta, typeEntity as object | undefined);
72
+
73
+ inProgress.delete(obj.id);
74
+ cache.set(obj.id, result);
75
+ return result;
76
+ };
51
77
 
52
- return makeObject(schema, props, meta, typeEntity as object | undefined);
78
+ /**
79
+ * Whether `entity`'s parent chain reaches `root` — i.e. it is an owned descendant that cascade-deletes with
80
+ * the root. Walks the `ParentId` back-links (a finite tree); guarded against cycles defensively.
81
+ */
82
+ const isChildOf = (entity: unknown, root: Obj.Any): boolean => {
83
+ const seen = new Set<InternalObjectProps>();
84
+ assumeType<InternalObjectProps>(entity);
85
+ let parent = entity[ParentId];
86
+ while (parent != null && !seen.has(parent)) {
87
+ if (parent.id === root.id) {
88
+ return true;
89
+ }
90
+ seen.add(parent);
91
+ parent = parent[ParentId];
92
+ }
93
+ return false;
53
94
  };
@@ -54,13 +54,13 @@ describe('create (static version)', () => {
54
54
 
55
55
  const json = JSON.parse(JSON.stringify(contact));
56
56
  expect(json).toEqual({
57
- id: contact.id,
57
+ 'id': contact.id,
58
58
  '@type': DXN.make(Type.getTypename(TestSchema.Person), Type.getVersion(TestSchema.Person)),
59
59
  '@meta': {
60
60
  keys: [],
61
61
  },
62
- name: 'Bot',
63
- email: 'bot@example.com',
62
+ 'name': 'Bot',
63
+ 'email': 'bot@example.com',
64
64
  });
65
65
  expect(objectToJSON(contact)).toStrictEqual(json);
66
66
  });
@@ -55,10 +55,9 @@ export type CreateObjectProps<T> = (T extends { id: string }
55
55
  *
56
56
  * @example
57
57
  * ```ts
58
- * const Contact = Schema.Struct({
59
- * name: Schema.String,
60
- * email: Schema.String,
61
- * }).pipe(Type.makeObject(DXN.make('com.example.type.person', '0.1.0')))
58
+ * class Contact extends Type.makeObject<Contact>(DXN.make('com.example.type.person', '0.1.0'))(
59
+ * Schema.Struct({ name: Schema.String, email: Schema.String }),
60
+ * ) {}
62
61
  *
63
62
  * const contact = createObject(Contact, {
64
63
  * name: "John",
@@ -7,7 +7,7 @@ import type { InspectOptionsStylized, inspect as inspectFn } from 'node:util';
7
7
  import { type CustomInspectFunction, inspectCustom } from '@dxos/debug';
8
8
 
9
9
  import { getTypeURI } from '../Annotation';
10
- import { ATTR_TYPE, type AnyEntity } from '../common/types';
10
+ import { type AnyEntity, ATTR_TYPE } from '../common/types';
11
11
  import { ATTR_META } from '../common/types/meta';
12
12
  import { MetaId } from '../common/types/model-symbols';
13
13
 
@@ -10,7 +10,7 @@ import { DXN, EID } from '@dxos/keys';
10
10
  import * as Obj from '../../Obj';
11
11
  import { TestSchema } from '../../testing';
12
12
  import * as Type from '../../Type';
13
- import { getTypeURI, getTypename } from '../Annotation';
13
+ import { getTypename, getTypeURI } from '../Annotation';
14
14
  import { getMetaChecked } from '../common/api';
15
15
  import { ATTR_TYPE, EntityKind, KindId, TypeId, getSchema } from '../common/types';
16
16
  import { MetaId } from '../common/types/model-symbols';
@@ -160,10 +160,12 @@ describe('Object JSON serializer', () => {
160
160
  });
161
161
 
162
162
  describe('Uint8Array', () => {
163
- const Blob = Schema.Struct({
164
- name: Schema.String,
165
- bytes: Schema.Uint8ArrayFromSelf,
166
- }).pipe(Type.makeObject(DXN.make('com.example.type.blob', '0.1.0')));
163
+ const Blob = Type.makeObject(DXN.make('com.example.type.blob', '0.1.0'))(
164
+ Schema.Struct({
165
+ name: Schema.String,
166
+ bytes: Schema.Uint8ArrayFromSelf,
167
+ }),
168
+ );
167
169
  type Blob = Type.InstanceType<typeof Blob>;
168
170
 
169
171
  test('round-trips Uint8Array field through JSON with schema', async ({ expect }) => {
@@ -15,9 +15,9 @@ import type * as Obj from '../../Obj';
15
15
  import { getTypeAnnotation, getTypeURI, setTypename } from '../Annotation';
16
16
  import { attachTypedJsonSerializer, defineHiddenProperty, typedJsonSerializer } from '../common/proxy';
17
17
  import {
18
+ type AnyEntity,
18
19
  ATTR_PARENT,
19
20
  ATTR_TYPE,
20
- type AnyEntity,
21
21
  EntityKind,
22
22
  KindId,
23
23
  ParentId,
@@ -129,6 +129,19 @@ describe('schema-validator', () => {
129
129
  valueToAssign: { any: 'value' },
130
130
  });
131
131
  });
132
+
133
+ test('walking into an Unknown-typed value does not throw', ({ expect }) => {
134
+ // Mirrors the annotation Dictionary (Record<string, Unknown>): nested keys/indices under an
135
+ // Unknown slot are all valid, so the validator must not reject while walking them.
136
+ const schema = Schema.Struct({ dict: Schema.Record({ key: Schema.String, value: Schema.Unknown }) });
137
+ for (const path of [
138
+ ['dict', 'someType'],
139
+ ['dict', 'someType', 'nested'],
140
+ ['dict', 'someType', '0'],
141
+ ]) {
142
+ SchemaValidator.getPropertySchema(schema, path);
143
+ }
144
+ });
132
145
  test('index signatures', () => {
133
146
  for (const value of [42, '42']) {
134
147
  validateValueToAssign({
@@ -179,4 +192,44 @@ describe('schema-validator', () => {
179
192
  }
180
193
  });
181
194
  });
195
+
196
+ describe('assertExactProperties', () => {
197
+ test('rejects extra properties on a closed struct', () => {
198
+ const Person = Schema.Struct({ name: Schema.String });
199
+ expect(() => SchemaValidator.assertExactProperties(Person, { name: 'Alice', extra: true })).to.throw(
200
+ /Unknown property: extra/,
201
+ );
202
+ });
203
+
204
+ test('allows extra properties when the schema has an index signature', () => {
205
+ const Expando = Schema.Struct({}, { key: Schema.String, value: Schema.Any });
206
+ expect(() => SchemaValidator.assertExactProperties(Expando, { custom: 'value' })).not.to.throw();
207
+ });
208
+
209
+ test('rejects extra nested properties', () => {
210
+ const schema = Schema.Struct({
211
+ nested: Schema.Struct({ field: Schema.Number }),
212
+ });
213
+ expect(() => SchemaValidator.assertExactProperties(schema, { nested: { field: 1, extra: true } })).to.throw(
214
+ /Unknown property: nested.extra/,
215
+ );
216
+ });
217
+
218
+ test('resolves nested discriminated union using target property values', () => {
219
+ const spec = Schema.Union(
220
+ Schema.Struct({ kind: Schema.Literal('feed'), feed: Schema.optional(Schema.String) }),
221
+ Schema.Struct({ kind: Schema.Literal('timer'), cron: Schema.String }),
222
+ );
223
+ const schema = Schema.Struct({ spec });
224
+ const target = { spec: { kind: 'feed' as const, feed: 'echo:/feed', extra: true } };
225
+ expect(() => SchemaValidator.assertExactProperties(schema, target, (path) => getDeep(target, path))).to.throw(
226
+ /Unknown property: spec.extra/,
227
+ );
228
+ expect(() =>
229
+ SchemaValidator.assertExactProperties(schema, { spec: { kind: 'feed' as const } }, (path) =>
230
+ getDeep({ spec: { kind: 'feed' as const } }, path),
231
+ ),
232
+ ).not.to.throw();
233
+ });
234
+ });
182
235
  });