@dxos/echo 0.8.4-main.d05673bc65 → 0.8.4-main.dfabb4ec29
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/README.md +2 -2
- package/dist/lib/neutral/Annotation.mjs +5 -3
- package/dist/lib/neutral/Database.mjs +8 -4
- package/dist/lib/neutral/Entity.mjs +16 -14
- package/dist/lib/neutral/Err.mjs +1 -1
- package/dist/lib/neutral/Extension.mjs +18 -0
- package/dist/lib/neutral/Extension.mjs.map +7 -0
- package/dist/lib/neutral/Feed.mjs +21 -17
- package/dist/lib/neutral/Filter.mjs +21 -13
- package/dist/lib/neutral/Format.mjs +3 -3
- package/dist/lib/neutral/JsonSchema.mjs +8 -8
- package/dist/lib/neutral/Key.mjs +1 -1
- package/dist/lib/neutral/Migration.mjs +17 -0
- package/dist/lib/neutral/Migration.mjs.map +7 -0
- package/dist/lib/neutral/Obj.mjs +16 -13
- package/dist/lib/neutral/Order.mjs +1 -1
- package/dist/lib/neutral/Query.mjs +19 -16
- package/dist/lib/neutral/QueryResult.mjs +1 -1
- package/dist/lib/neutral/Ref.mjs +7 -7
- package/dist/lib/neutral/Relation.mjs +15 -14
- package/dist/lib/neutral/SchemaRegistry.mjs +1 -1
- package/dist/lib/neutral/Tag.mjs +14 -13
- package/dist/lib/neutral/Type.mjs +10 -10
- package/dist/lib/neutral/{chunk-FZO7LQO7.mjs → chunk-44HT3MEC.mjs} +2 -2
- package/dist/lib/neutral/{chunk-OENWMTE6.mjs → chunk-5SL5LDLD.mjs} +4 -2
- package/dist/lib/neutral/chunk-5SL5LDLD.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-YWXWXIE5.mjs → chunk-6VIJV543.mjs} +2 -2
- package/dist/lib/neutral/{chunk-YWXWXIE5.mjs.map → chunk-6VIJV543.mjs.map} +2 -2
- package/dist/lib/neutral/{chunk-ANHVGJI4.mjs → chunk-7RVZT53K.mjs} +1 -1
- package/dist/lib/neutral/{chunk-BNCCGLJN.mjs → chunk-BICZKPQG.mjs} +1 -1
- package/dist/lib/neutral/{chunk-NEGC54NE.mjs → chunk-FHYIM4RD.mjs} +17 -19
- package/dist/lib/neutral/chunk-FHYIM4RD.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-RMLF7JOZ.mjs → chunk-FZHVQEHN.mjs} +6 -22
- package/dist/lib/neutral/{chunk-RMLF7JOZ.mjs.map → chunk-FZHVQEHN.mjs.map} +3 -3
- package/dist/lib/neutral/chunk-GWFFC34K.mjs +50 -0
- package/dist/lib/neutral/chunk-GWFFC34K.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-724GQYEN.mjs → chunk-HPNQTEEQ.mjs} +4 -4
- package/dist/lib/neutral/{chunk-UFDK26FO.mjs → chunk-LVGOVFDV.mjs} +12 -8
- package/dist/lib/neutral/chunk-LVGOVFDV.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-43Y5DOS6.mjs → chunk-MPAI2MHO.mjs} +16 -66
- package/dist/lib/neutral/chunk-MPAI2MHO.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-WYOKA6AE.mjs → chunk-N4B7FHQT.mjs} +2 -2
- package/dist/lib/neutral/{chunk-WYOKA6AE.mjs.map → chunk-N4B7FHQT.mjs.map} +1 -1
- package/dist/lib/neutral/{chunk-BOZZPUXE.mjs → chunk-O5LRY6CO.mjs} +12 -65
- package/dist/lib/neutral/chunk-O5LRY6CO.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-UTBRYVQC.mjs → chunk-QGMIH2SN.mjs} +2 -2
- package/dist/lib/neutral/{chunk-VA2F47ZC.mjs → chunk-QXIANHKU.mjs} +68 -14
- package/dist/lib/neutral/chunk-QXIANHKU.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-NXMFBIT5.mjs → chunk-RF7ZBZ4A.mjs} +59 -4
- package/dist/lib/neutral/chunk-RF7ZBZ4A.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-DYDJBZEG.mjs → chunk-S7IMFVTB.mjs} +51 -21
- package/dist/lib/neutral/chunk-S7IMFVTB.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-TQQZLKB7.mjs → chunk-SJKBWMJY.mjs} +193 -469
- package/dist/lib/neutral/chunk-SJKBWMJY.mjs.map +7 -0
- package/dist/lib/neutral/chunk-SUZMWP3Y.mjs +36 -0
- package/dist/lib/neutral/chunk-SUZMWP3Y.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-NQRLRYJM.mjs → chunk-SW5CUSBY.mjs} +119 -12
- package/dist/lib/neutral/chunk-SW5CUSBY.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-SEMVAGBM.mjs → chunk-TNBK56IN.mjs} +19 -24
- package/dist/lib/neutral/chunk-TNBK56IN.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-B5OXLWZL.mjs → chunk-TRPZU2HV.mjs} +2 -2
- package/dist/lib/neutral/{chunk-UI6MWK5W.mjs → chunk-TTCSATUD.mjs} +1 -1
- package/dist/lib/neutral/{chunk-OMUPQMLR.mjs → chunk-V72DY6LU.mjs} +1 -1
- package/dist/lib/neutral/{chunk-KTAWP7GI.mjs → chunk-VGKLHHRT.mjs} +42 -7
- package/dist/lib/neutral/chunk-VGKLHHRT.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-FXEG7EOK.mjs → chunk-VYAGNFSJ.mjs} +3 -3
- package/dist/lib/neutral/{chunk-CIRHIEMW.mjs → chunk-WRVRDZDA.mjs} +18 -46
- package/dist/lib/neutral/chunk-WRVRDZDA.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-UBEZSGXY.mjs → chunk-ZISMEVKD.mjs} +1 -1
- package/dist/lib/neutral/{chunk-UBEZSGXY.mjs.map → chunk-ZISMEVKD.mjs.map} +2 -2
- package/dist/lib/neutral/index.mjs +38 -28
- package/dist/lib/neutral/internal/index.mjs +15 -9
- package/dist/lib/neutral/meta.json +1 -1
- package/dist/lib/neutral/testing/index.mjs +185 -129
- package/dist/lib/neutral/testing/index.mjs.map +3 -3
- package/dist/types/src/Annotation.d.ts +1 -1
- package/dist/types/src/Annotation.d.ts.map +1 -1
- package/dist/types/src/Collection.d.ts.map +1 -1
- package/dist/types/src/Database.d.ts +13 -1
- package/dist/types/src/Database.d.ts.map +1 -1
- package/dist/types/src/Dataset.d.ts +2 -1
- package/dist/types/src/Dataset.d.ts.map +1 -1
- package/dist/types/src/Entity.d.ts +17 -11
- package/dist/types/src/Entity.d.ts.map +1 -1
- package/dist/types/src/Err.d.ts +18 -18
- package/dist/types/src/Err.d.ts.map +1 -1
- package/dist/types/src/Extension.d.ts +80 -0
- package/dist/types/src/Extension.d.ts.map +1 -0
- package/dist/types/src/Extension.test.d.ts +2 -0
- package/dist/types/src/Extension.test.d.ts.map +1 -0
- package/dist/types/src/Feed.d.ts +37 -20
- package/dist/types/src/Feed.d.ts.map +1 -1
- package/dist/types/src/Filter.d.ts +33 -4
- package/dist/types/src/Filter.d.ts.map +1 -1
- package/dist/types/src/Filter.test.d.ts +2 -0
- package/dist/types/src/Filter.test.d.ts.map +1 -0
- package/dist/types/src/Json.d.ts +33 -0
- package/dist/types/src/Json.d.ts.map +1 -0
- package/dist/types/src/Json.test.d.ts +2 -0
- package/dist/types/src/Json.test.d.ts.map +1 -0
- package/dist/types/src/JsonSchema.d.ts +1 -1
- package/dist/types/src/Migration.d.ts +57 -0
- package/dist/types/src/Migration.d.ts.map +1 -0
- package/dist/types/src/Obj.d.ts +41 -27
- package/dist/types/src/Obj.d.ts.map +1 -1
- package/dist/types/src/Order.d.ts.map +1 -1
- package/dist/types/src/Query.d.ts +13 -2
- package/dist/types/src/Query.d.ts.map +1 -1
- package/dist/types/src/Ref.d.ts.map +1 -1
- package/dist/types/src/Relation.d.ts +17 -18
- package/dist/types/src/Relation.d.ts.map +1 -1
- package/dist/types/src/Tag.d.ts +2 -2
- package/dist/types/src/Tag.d.ts.map +1 -1
- package/dist/types/src/Type.d.ts +3 -3
- package/dist/types/src/Type.d.ts.map +1 -1
- package/dist/types/src/View.d.ts +1 -1
- package/dist/types/src/View.d.ts.map +1 -1
- package/dist/types/src/exemplars.test.d.ts +2 -0
- package/dist/types/src/exemplars.test.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +3 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/internal/Annotation/annotations.d.ts +12 -2
- package/dist/types/src/internal/Annotation/annotations.d.ts.map +1 -1
- package/dist/types/src/internal/Annotation/sorting.d.ts.map +1 -1
- package/dist/types/src/internal/Annotation/util.d.ts +1 -1
- package/dist/types/src/internal/Annotation/util.d.ts.map +1 -1
- package/dist/types/src/internal/Entity/api.d.ts.map +1 -1
- package/dist/types/src/internal/Entity/object.d.ts.map +1 -1
- package/dist/types/src/internal/Entity/relation.d.ts.map +1 -1
- package/dist/types/src/internal/Entity/version.d.ts.map +1 -1
- package/dist/types/src/internal/Format/date.d.ts.map +1 -1
- package/dist/types/src/internal/Format/format.d.ts.map +1 -1
- package/dist/types/src/internal/Format/number.d.ts.map +1 -1
- package/dist/types/src/internal/Format/object.d.ts.map +1 -1
- package/dist/types/src/internal/Format/types.d.ts.map +1 -1
- package/dist/types/src/internal/JsonSchema/json-schema-normalize.d.ts.map +1 -1
- package/dist/types/src/internal/JsonSchema/json-schema-type.d.ts +28 -28
- package/dist/types/src/internal/JsonSchema/json-schema-type.d.ts.map +1 -1
- package/dist/types/src/internal/JsonSchema/json-schema.d.ts +1 -1
- package/dist/types/src/internal/JsonSchema/json-schema.d.ts.map +1 -1
- package/dist/types/src/internal/Obj/clone.d.ts.map +1 -1
- package/dist/types/src/internal/Obj/common.d.ts.map +1 -1
- package/dist/types/src/internal/Obj/create-object.d.ts.map +1 -1
- package/dist/types/src/internal/Obj/deleted.d.ts.map +1 -1
- package/dist/types/src/internal/Obj/ids.d.ts.map +1 -1
- package/dist/types/src/internal/Obj/json-serializer.d.ts +4 -3
- package/dist/types/src/internal/Obj/json-serializer.d.ts.map +1 -1
- package/dist/types/src/internal/Obj/set-value.d.ts +1 -1
- package/dist/types/src/internal/Obj/set-value.d.ts.map +1 -1
- package/dist/types/src/internal/Obj/snapshot.d.ts.map +1 -1
- package/dist/types/src/internal/Query.d.ts +10 -0
- package/dist/types/src/internal/Query.d.ts.map +1 -0
- package/dist/types/src/internal/Ref/ref-array.d.ts.map +1 -1
- package/dist/types/src/internal/Ref/ref.d.ts +14 -1
- package/dist/types/src/internal/Ref/ref.d.ts.map +1 -1
- package/dist/types/src/internal/Type/compose.d.ts.map +1 -1
- package/dist/types/src/internal/Type/echo-schema.d.ts +2 -2
- package/dist/types/src/internal/Type/echo-schema.d.ts.map +1 -1
- package/dist/types/src/internal/Type/manipulation.d.ts.map +1 -1
- package/dist/types/src/internal/common/api/meta.d.ts +3 -3
- package/dist/types/src/internal/common/api/meta.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/change-context.d.ts +1 -1
- package/dist/types/src/internal/common/proxy/change-context.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/define-hidden-property.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/errors.d.ts +1 -1
- package/dist/types/src/internal/common/proxy/errors.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/event-batch.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/json-serializer.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/make-object.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/ownership.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/proxy-utils.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/reactive-array.d.ts +1 -1
- package/dist/types/src/internal/common/proxy/reactive.d.ts +1 -1
- package/dist/types/src/internal/common/proxy/reactive.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/reactive.test.d.ts +2 -0
- package/dist/types/src/internal/common/proxy/reactive.test.d.ts.map +1 -0
- package/dist/types/src/internal/common/proxy/schema-validator.d.ts.map +1 -1
- package/dist/types/src/internal/common/proxy/typed-handler.d.ts.map +1 -1
- package/dist/types/src/internal/common/types/base.d.ts.map +1 -1
- package/dist/types/src/internal/common/types/entity.d.ts +4 -4
- package/dist/types/src/internal/common/types/entity.d.ts.map +1 -1
- package/dist/types/src/internal/common/types/meta.d.ts.map +1 -1
- package/dist/types/src/internal/common/types/version.d.ts +1 -1
- package/dist/types/src/internal/index.d.ts +1 -0
- package/dist/types/src/internal/index.d.ts.map +1 -1
- package/dist/types/src/testing/test-data.d.ts +8 -8
- package/dist/types/src/testing/test-data.d.ts.map +1 -1
- package/dist/types/src/testing/test-schema.d.ts +53 -53
- package/dist/types/src/testing/test-schema.d.ts.map +1 -1
- package/dist/types/src/testing/util.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +24 -14
- package/src/Annotation.ts +1 -0
- package/src/Collection.ts +5 -0
- package/src/Database.ts +49 -14
- package/src/Entity.ts +18 -12
- package/src/Extension.test.ts +235 -0
- package/src/Extension.ts +122 -0
- package/src/Feed.ts +79 -32
- package/src/Filter.test.ts +90 -0
- package/src/Filter.ts +66 -3
- package/src/Json.test.ts +175 -0
- package/src/Json.ts +102 -0
- package/src/Migration.ts +94 -0
- package/src/Obj.test.ts +105 -13
- package/src/Obj.ts +153 -33
- package/src/Query.test.ts +198 -8
- package/src/Query.ts +46 -1
- package/src/Relation.ts +24 -20
- package/src/Type.ts +1 -1
- package/src/View.ts +5 -0
- package/src/exemplars.test.ts +21 -0
- package/src/index.ts +4 -0
- package/src/internal/Annotation/annotations.test.ts +51 -2
- package/src/internal/Annotation/annotations.ts +33 -14
- package/src/internal/Annotation/sorting.ts +0 -1
- package/src/internal/Entity/api.ts +0 -1
- package/src/internal/Entity/object.ts +0 -1
- package/src/internal/Entity/version.ts +0 -1
- package/src/internal/Format/date.test.ts +0 -1
- package/src/internal/Format/format.test.ts +0 -1
- package/src/internal/JsonSchema/json-schema-type.ts +1 -1
- package/src/internal/JsonSchema/json-schema.test.ts +1 -2
- package/src/internal/JsonSchema/json-schema.ts +1 -2
- package/src/internal/Obj/clone.ts +1 -1
- package/src/internal/Obj/create-object.test.ts +2 -4
- package/src/internal/Obj/create-object.ts +2 -3
- package/src/internal/Obj/deleted.ts +1 -1
- package/src/internal/Obj/json-serializer.test.ts +4 -5
- package/src/internal/Obj/json-serializer.ts +22 -15
- package/src/internal/Obj/set-value.test.ts +24 -24
- package/src/internal/Obj/set-value.ts +1 -1
- package/src/internal/Query.ts +137 -0
- package/src/internal/Ref/ref-array.ts +0 -1
- package/src/internal/Ref/ref.test.ts +0 -1
- package/src/internal/Ref/ref.ts +18 -1
- package/src/internal/Type/compose.test.ts +0 -1
- package/src/internal/Type/echo-schema.ts +3 -4
- package/src/internal/Type/persistent-schema.ts +2 -2
- package/src/internal/common/README.md +1 -1
- package/src/internal/common/api/meta.ts +3 -3
- package/src/internal/common/proxy/change-context.ts +1 -1
- package/src/internal/common/proxy/change.test.ts +94 -94
- package/src/internal/common/proxy/errors.ts +2 -2
- package/src/internal/common/proxy/handler.test.ts +0 -2
- package/src/internal/common/proxy/make-object.ts +0 -1
- package/src/internal/common/proxy/ownership.ts +0 -1
- package/src/internal/common/proxy/reactive-array.ts +1 -1
- package/src/internal/common/proxy/reactive.test.ts +54 -0
- package/src/internal/common/proxy/reactive.ts +11 -3
- package/src/internal/common/proxy/typed-handler.test.ts +0 -1
- package/src/internal/common/proxy/typed-handler.ts +8 -10
- package/src/internal/common/proxy/typed-object.test.ts +2 -3
- package/src/internal/common/types/entity.ts +1 -1
- package/src/internal/common/types/meta.ts +0 -1
- package/src/internal/index.ts +1 -0
- package/src/testing/api.test.ts +0 -1
- package/src/testing/test-data.ts +157 -98
- package/dist/lib/neutral/chunk-43Y5DOS6.mjs.map +0 -7
- package/dist/lib/neutral/chunk-BOZZPUXE.mjs.map +0 -7
- package/dist/lib/neutral/chunk-CIRHIEMW.mjs.map +0 -7
- package/dist/lib/neutral/chunk-DYDJBZEG.mjs.map +0 -7
- package/dist/lib/neutral/chunk-KTAWP7GI.mjs.map +0 -7
- package/dist/lib/neutral/chunk-NEGC54NE.mjs.map +0 -7
- package/dist/lib/neutral/chunk-NQRLRYJM.mjs.map +0 -7
- package/dist/lib/neutral/chunk-NXMFBIT5.mjs.map +0 -7
- package/dist/lib/neutral/chunk-OENWMTE6.mjs.map +0 -7
- package/dist/lib/neutral/chunk-SEMVAGBM.mjs.map +0 -7
- package/dist/lib/neutral/chunk-TQQZLKB7.mjs.map +0 -7
- package/dist/lib/neutral/chunk-UFDK26FO.mjs.map +0 -7
- package/dist/lib/neutral/chunk-VA2F47ZC.mjs.map +0 -7
- /package/dist/lib/neutral/{chunk-FZO7LQO7.mjs.map → chunk-44HT3MEC.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-ANHVGJI4.mjs.map → chunk-7RVZT53K.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-BNCCGLJN.mjs.map → chunk-BICZKPQG.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-724GQYEN.mjs.map → chunk-HPNQTEEQ.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-UTBRYVQC.mjs.map → chunk-QGMIH2SN.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-B5OXLWZL.mjs.map → chunk-TRPZU2HV.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-UI6MWK5W.mjs.map → chunk-TTCSATUD.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-OMUPQMLR.mjs.map → chunk-V72DY6LU.mjs.map} +0 -0
- /package/dist/lib/neutral/{chunk-FXEG7EOK.mjs.map → chunk-VYAGNFSJ.mjs.map} +0 -0
package/src/Json.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { DXN } from '@dxos/keys';
|
|
6
|
+
|
|
7
|
+
import * as Database from './Database';
|
|
8
|
+
import * as Obj from './Obj';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `JSON.stringify` replacer signature.
|
|
12
|
+
*
|
|
13
|
+
* Defined here (rather than re-imported from a UI package) so other ECHO-aware utilities can
|
|
14
|
+
* share a stable signature without creating a dependency edge into the UI tree.
|
|
15
|
+
*/
|
|
16
|
+
export type JsonReplacer = (key: string, value: any) => any;
|
|
17
|
+
|
|
18
|
+
export type CreateRefReplacerOptions = {
|
|
19
|
+
db: Database.Database;
|
|
20
|
+
/** How many ref hops to follow. `0` leaves all refs as-is. Default: `1`. */
|
|
21
|
+
depth?: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const isEncodedRef = (value: unknown): value is { '/': string } =>
|
|
25
|
+
typeof value === 'object' &&
|
|
26
|
+
value !== null &&
|
|
27
|
+
Object.keys(value as object).length === 1 &&
|
|
28
|
+
typeof (value as { '/': unknown })['/'] === 'string';
|
|
29
|
+
|
|
30
|
+
const toJson = (obj: Obj.Any): unknown => (typeof (obj as any).toJSON === 'function' ? (obj as any).toJSON() : obj);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Returns a {@link JsonReplacer} that inlines ECHO ref objects (`{ "/": "dxn:echo:..." }`) up to
|
|
34
|
+
* `depth` ref hops. Beyond that depth refs are left in their encoded form.
|
|
35
|
+
*
|
|
36
|
+
* Implemented as a per-call `JSON.stringify` replacer (not a one-shot tree walk at root) so it
|
|
37
|
+
* composes with wrappers like `safeStringify` that intercept the root call. JSON.stringify
|
|
38
|
+
* already drives the recursion; we only need to (a) detect a ref at the current callback,
|
|
39
|
+
* (b) resolve and return the target if hop budget remains, and (c) tag the returned object
|
|
40
|
+
* with its hop count so children know how far in they are.
|
|
41
|
+
*
|
|
42
|
+
* The hop count is tracked per-object via a `WeakMap`: a ref-resolved target's children inherit
|
|
43
|
+
* `parentHops + 1`; a regular intermediate object's children inherit `parentHops`. This makes the
|
|
44
|
+
* budget count *ref hops*, not tree depth — a ref deep in a tree still resolves once when
|
|
45
|
+
* `depth >= 1`.
|
|
46
|
+
*
|
|
47
|
+
* Note: ECHO objects' `toJSON` runs before the replacer is invoked, so by the time we see a
|
|
48
|
+
* value refs are already encoded as `{ "/": "dxn:..." }`.
|
|
49
|
+
*/
|
|
50
|
+
export const createRefReplacer = ({ db, depth = 1 }: CreateRefReplacerOptions): JsonReplacer => {
|
|
51
|
+
// Per-object hop count. Set when we return an object (via ref resolution or pass-through) so
|
|
52
|
+
// the child callbacks (which carry that object as `this`) can read it.
|
|
53
|
+
const hops = new WeakMap<object, number>();
|
|
54
|
+
|
|
55
|
+
return function (this: any, key: string, value: any) {
|
|
56
|
+
// Hop count for this call: hops at the parent, or 0 for the root.
|
|
57
|
+
const parentHops = this && typeof this === 'object' ? (hops.get(this) ?? 0) : 0;
|
|
58
|
+
if (isEncodedRef(value)) {
|
|
59
|
+
if (parentHops >= depth) {
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The `{ '/': string }` shape is shared with non-DXN IPLD-style refs (e.g. CIDs);
|
|
64
|
+
// an unparseable string would otherwise crash the whole `JSON.stringify`.
|
|
65
|
+
// Treat any parse miss as "leave as-is" rather than propagating.
|
|
66
|
+
const dxnString = value['/'];
|
|
67
|
+
if (!dxnString.startsWith('dxn:')) {
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let echoId: string | undefined;
|
|
72
|
+
try {
|
|
73
|
+
echoId = DXN.parse(dxnString).asEchoDXN()?.echoId;
|
|
74
|
+
} catch {
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!echoId) {
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
const target = db.getObjectById(echoId);
|
|
82
|
+
if (!target) {
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const encoded = toJson(target);
|
|
87
|
+
if (encoded != null && typeof encoded === 'object') {
|
|
88
|
+
// Children of the resolved target are one hop deeper.
|
|
89
|
+
hops.set(encoded as object, parentHops + 1);
|
|
90
|
+
}
|
|
91
|
+
return encoded;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Pass-through object: children inherit the parent's hop count (this branch doesn't burn
|
|
95
|
+
// budget).
|
|
96
|
+
if (value != null && typeof value === 'object') {
|
|
97
|
+
hops.set(value, parentHops);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return value;
|
|
101
|
+
};
|
|
102
|
+
};
|
package/src/Migration.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// @import-as-namespace
|
|
6
|
+
|
|
7
|
+
import type * as Schema from 'effect/Schema';
|
|
8
|
+
|
|
9
|
+
import { type DXN } from '@dxos/keys';
|
|
10
|
+
|
|
11
|
+
import type * as Database from './Database';
|
|
12
|
+
import type * as Entity from './Entity';
|
|
13
|
+
import { getSchemaDXN } from './internal';
|
|
14
|
+
|
|
15
|
+
type DefineObjectMigrationOptions<From extends Schema.Schema.AnyNoContext, To extends Schema.Schema.AnyNoContext> = {
|
|
16
|
+
from: From;
|
|
17
|
+
to: To;
|
|
18
|
+
/**
|
|
19
|
+
* Pure function that converts the old object data to the new object data.
|
|
20
|
+
*/
|
|
21
|
+
// TODO(dmaretskyi): `id` should not be a part of the schema.
|
|
22
|
+
transform: (
|
|
23
|
+
from: Schema.Schema.Type<From>,
|
|
24
|
+
context: ObjectMigrationContext,
|
|
25
|
+
) => Promise<Omit<Schema.Schema.Type<To>, 'id' | Entity.KindId>>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Callback that is called after the object is migrated. Called for every object that is migrated.
|
|
29
|
+
*
|
|
30
|
+
* NOTE: Database mutations performed in this callback are not guaranteed to be idempotent.
|
|
31
|
+
* If multiple peers run the migration separately, the effects may be applied multiple times.
|
|
32
|
+
*/
|
|
33
|
+
onMigration: (params: OnMigrateProps<From, To>) => Promise<void>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Context passed to object migration callbacks.
|
|
38
|
+
*/
|
|
39
|
+
export type ObjectMigrationContext = {
|
|
40
|
+
db: Database.Database;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type OnMigrateProps<From extends Schema.Schema.AnyNoContext, To extends Schema.Schema.AnyNoContext> = {
|
|
44
|
+
before: Schema.Schema.Type<From>;
|
|
45
|
+
object: Schema.Schema.Type<To>;
|
|
46
|
+
db: Database.Database;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Definition of a migration from one object schema version to another.
|
|
51
|
+
*/
|
|
52
|
+
export type ObjectMigration = {
|
|
53
|
+
fromType: DXN;
|
|
54
|
+
toType: DXN;
|
|
55
|
+
fromSchema: Schema.Schema.AnyNoContext;
|
|
56
|
+
toSchema: Schema.Schema.AnyNoContext;
|
|
57
|
+
transform: (from: unknown, context: ObjectMigrationContext) => Promise<unknown>;
|
|
58
|
+
onMigration: (params: OnMigrateProps<any, any>) => Promise<void>;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Define a migration between two object schemas.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```ts
|
|
66
|
+
* const migration = Migration.define({
|
|
67
|
+
* from: ContactV1,
|
|
68
|
+
* to: ContactV2,
|
|
69
|
+
* transform: async (from) => ({ name: `${from.firstName} ${from.lastName}` }),
|
|
70
|
+
* onMigration: async () => {},
|
|
71
|
+
* });
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export const define = <From extends Schema.Schema.AnyNoContext, To extends Schema.Schema.AnyNoContext>(
|
|
75
|
+
options: DefineObjectMigrationOptions<From, To>,
|
|
76
|
+
): ObjectMigration => {
|
|
77
|
+
const fromType = getSchemaDXN(options.from);
|
|
78
|
+
if (!fromType) {
|
|
79
|
+
throw new Error('Invalid from schema');
|
|
80
|
+
}
|
|
81
|
+
const toType = getSchemaDXN(options.to);
|
|
82
|
+
if (!toType) {
|
|
83
|
+
throw new Error('Invalid to schema');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
fromType,
|
|
88
|
+
toType,
|
|
89
|
+
fromSchema: options.from,
|
|
90
|
+
toSchema: options.to,
|
|
91
|
+
transform: options.transform as any,
|
|
92
|
+
onMigration: options.onMigration as any,
|
|
93
|
+
};
|
|
94
|
+
};
|
package/src/Obj.test.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2026 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { describe, expectTypeOf, test } from 'vitest';
|
|
5
|
+
import { describe, expectTypeOf, test, expect } from 'vitest';
|
|
6
6
|
|
|
7
7
|
import * as Entity from './Entity';
|
|
8
8
|
import { SnapshotKindId } from './internal';
|
|
@@ -117,8 +117,8 @@ describe('Obj', () => {
|
|
|
117
117
|
|
|
118
118
|
test('returns keys from reactive object and snapshot', ({ expect }) => {
|
|
119
119
|
const obj = Obj.make(TestSchema.Person, { name: 'Test' });
|
|
120
|
-
Obj.
|
|
121
|
-
const meta = Obj.getMeta(
|
|
120
|
+
Obj.update(obj, (obj) => {
|
|
121
|
+
const meta = Obj.getMeta(obj);
|
|
122
122
|
meta.keys.push({ source: SOURCE, id: 'key-1' });
|
|
123
123
|
meta.keys.push({ source: SOURCE, id: 'key-2' });
|
|
124
124
|
meta.keys.push({ source: 'other', id: 'key-3' });
|
|
@@ -166,8 +166,8 @@ describe('Obj', () => {
|
|
|
166
166
|
|
|
167
167
|
const cloned = Obj.clone(original);
|
|
168
168
|
|
|
169
|
-
Obj.
|
|
170
|
-
|
|
169
|
+
Obj.update(original, (original) => {
|
|
170
|
+
original.name = 'Bob';
|
|
171
171
|
});
|
|
172
172
|
|
|
173
173
|
expect(original.name).toBe('Bob');
|
|
@@ -206,8 +206,8 @@ describe('Obj', () => {
|
|
|
206
206
|
expect(cloned.employer?.target).toBe(person.employer?.target);
|
|
207
207
|
|
|
208
208
|
// Modifying the referenced object affects both
|
|
209
|
-
Obj.
|
|
210
|
-
|
|
209
|
+
Obj.update(employer, (employer) => {
|
|
210
|
+
employer.name = 'Updated DXOS';
|
|
211
211
|
});
|
|
212
212
|
|
|
213
213
|
expect(cloned.employer?.target?.name).toBe('Updated DXOS');
|
|
@@ -234,8 +234,8 @@ describe('Obj', () => {
|
|
|
234
234
|
expect(cloned.employer?.target?.name).toBe(employer.name);
|
|
235
235
|
|
|
236
236
|
// Modifying the original referenced object does not affect the clone
|
|
237
|
-
Obj.
|
|
238
|
-
|
|
237
|
+
Obj.update(employer, (employer) => {
|
|
238
|
+
employer.name = 'Updated DXOS';
|
|
239
239
|
});
|
|
240
240
|
|
|
241
241
|
expect(cloned.employer?.target?.name).toBe('DXOS');
|
|
@@ -333,8 +333,8 @@ describe('Obj', () => {
|
|
|
333
333
|
expect(cloned.address?.coordinates.lng).toBe(-122.4194);
|
|
334
334
|
|
|
335
335
|
// Modifying nested properties should be independent
|
|
336
|
-
Obj.
|
|
337
|
-
|
|
336
|
+
Obj.update(person, (person) => {
|
|
337
|
+
person.address!.city = 'New York';
|
|
338
338
|
});
|
|
339
339
|
|
|
340
340
|
expect(cloned.address?.city).toBe('San Francisco');
|
|
@@ -366,8 +366,8 @@ describe('Obj', () => {
|
|
|
366
366
|
expect(cloned.tasks?.[2]?.target).not.toBe(task3);
|
|
367
367
|
|
|
368
368
|
// Modifying original tasks should not affect cloned ones
|
|
369
|
-
Obj.
|
|
370
|
-
|
|
369
|
+
Obj.update(task1, (task1) => {
|
|
370
|
+
task1.title = 'Updated Task 1';
|
|
371
371
|
});
|
|
372
372
|
|
|
373
373
|
expect(cloned.tasks?.[0]?.target?.title).toBe('Task 1');
|
|
@@ -444,4 +444,96 @@ describe('Obj', () => {
|
|
|
444
444
|
expectTypeOf<Obj.Any>().toMatchTypeOf<Obj.Unknown>();
|
|
445
445
|
});
|
|
446
446
|
});
|
|
447
|
+
|
|
448
|
+
describe('Obj.updateFrom', () => {
|
|
449
|
+
test('returns false when values already match', () => {
|
|
450
|
+
const target = Obj.make(TestSchema.Organization, { name: 'Acme', properties: { region: 'EU' } });
|
|
451
|
+
const source = Obj.make(TestSchema.Organization, { name: 'Acme', properties: { region: 'EU' } });
|
|
452
|
+
Obj.update(target, (target) => {
|
|
453
|
+
expect(Obj.updateFrom(target, source)).toBe(false);
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test('updates scalar and nested record fields on Organization', () => {
|
|
458
|
+
const target = Obj.make(TestSchema.Organization, {
|
|
459
|
+
name: 'Old',
|
|
460
|
+
properties: { a: '1' },
|
|
461
|
+
});
|
|
462
|
+
const source = Obj.make(TestSchema.Organization, {
|
|
463
|
+
name: 'New',
|
|
464
|
+
properties: { a: '2', b: '3' },
|
|
465
|
+
});
|
|
466
|
+
Obj.update(target, (target) => {
|
|
467
|
+
expect(Obj.updateFrom(target, source)).toBe(true);
|
|
468
|
+
});
|
|
469
|
+
expect(target.name).toBe('New');
|
|
470
|
+
expect(target.properties).toEqual({ a: '2', b: '3' });
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test('compares employer refs by DXN and updates Person fields', () => {
|
|
474
|
+
const orgA = Obj.make(TestSchema.Organization, { name: 'A' });
|
|
475
|
+
const orgB = Obj.make(TestSchema.Organization, { name: 'B' });
|
|
476
|
+
const target = Obj.make(TestSchema.Person, {
|
|
477
|
+
name: 'Ann',
|
|
478
|
+
username: 'ann',
|
|
479
|
+
email: 'ann@x.test',
|
|
480
|
+
employer: Ref.make(orgA),
|
|
481
|
+
address: { city: 'X', state: 'Y', zip: '1', coordinates: { lat: 0, lng: 0 } },
|
|
482
|
+
});
|
|
483
|
+
const source = Obj.make(TestSchema.Person, {
|
|
484
|
+
name: 'Ann',
|
|
485
|
+
username: 'ann',
|
|
486
|
+
email: 'ann@x.test',
|
|
487
|
+
employer: Ref.make(orgB),
|
|
488
|
+
address: { city: 'Portland', state: 'OR', zip: '97201', coordinates: { lat: 45.5, lng: -122.6 } },
|
|
489
|
+
});
|
|
490
|
+
Obj.update(target, (target) => {
|
|
491
|
+
expect(Obj.updateFrom(target, source)).toBe(true);
|
|
492
|
+
});
|
|
493
|
+
expect(target.employer?.dxn.toString()).toBe(Ref.make(orgB).dxn.toString());
|
|
494
|
+
expect(target.address?.city).toBe('Portland');
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
test('updates array of task refs when DXNs differ', () => {
|
|
498
|
+
const t1 = Obj.make(TestSchema.Task, { title: 'One' });
|
|
499
|
+
const t2 = Obj.make(TestSchema.Task, { title: 'Two' });
|
|
500
|
+
const t3 = Obj.make(TestSchema.Task, { title: 'Three' });
|
|
501
|
+
const target = Obj.make(TestSchema.Person, {
|
|
502
|
+
name: 'Bob',
|
|
503
|
+
username: 'bob',
|
|
504
|
+
email: 'bob@x.test',
|
|
505
|
+
tasks: [Ref.make(t1), Ref.make(t2)],
|
|
506
|
+
});
|
|
507
|
+
const source = Obj.make(TestSchema.Person, {
|
|
508
|
+
name: 'Bob',
|
|
509
|
+
username: 'bob',
|
|
510
|
+
email: 'bob@x.test',
|
|
511
|
+
tasks: [Ref.make(t1), Ref.make(t3)],
|
|
512
|
+
});
|
|
513
|
+
Obj.update(target, (target) => {
|
|
514
|
+
expect(Obj.updateFrom(target, source)).toBe(true);
|
|
515
|
+
});
|
|
516
|
+
expect(target.tasks?.map((r) => r.dxn.toString())).toEqual(source.tasks?.map((r) => r.dxn.toString()));
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test('respects include option', () => {
|
|
520
|
+
const target = Obj.make(TestSchema.Organization, { name: 'Keep', properties: { x: '1' } });
|
|
521
|
+
const source = Obj.make(TestSchema.Organization, { name: 'Drop', properties: { x: '2' } });
|
|
522
|
+
Obj.update(target, (target) => {
|
|
523
|
+
expect(Obj.updateFrom(target, source, { include: ['properties'] })).toBe(true);
|
|
524
|
+
});
|
|
525
|
+
expect(target.name).toBe('Keep');
|
|
526
|
+
expect(target.properties).toEqual({ x: '2' });
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
test('respects exclude option', () => {
|
|
530
|
+
const target = Obj.make(TestSchema.Organization, { name: 'Old', properties: { x: '1' } });
|
|
531
|
+
const source = Obj.make(TestSchema.Organization, { name: 'New', properties: { x: '2' } });
|
|
532
|
+
Obj.update(target, (target) => {
|
|
533
|
+
expect(Obj.updateFrom(target, source, { exclude: ['name'] })).toBe(true);
|
|
534
|
+
});
|
|
535
|
+
expect(target.name).toBe('Old');
|
|
536
|
+
expect(target.properties).toEqual({ x: '2' });
|
|
537
|
+
});
|
|
538
|
+
});
|
|
447
539
|
});
|