Mutating objects
By the end of this page the app will add, edit and remove records, and you will watch a record change.
Before you start
Section titled “Before you start”An object you make with Obj.make exists only in memory until you add it to a database. db.add starts tracking it: from then on the database stores every change and sends it to the space’s other peers.
A tracked object is read like any other, but it is changed only inside Obj.update. The callback gets a mutable view of the object. Each update is one change, which peers apply whole, and which notifies subscribers once.
Writes are local. db.add and Obj.update return once the change is in the local database, and replication runs after. Two peers that change the same object concurrently both keep their edits: ECHO merges them field by field.
-
Write the app’s add functions. Each takes the database and the props of one type, and returns the tracked object.
export constaddOrganization = (const addOrganization: (db: Database.Database, props: {name: string;website?: string;}) => BaseObj & Organization & OfKind<EntityKind.Object>db:db: Database.DatabaseDatabase.import DatabaseDatabase,interface DatabaseECHO Database interface.
props: {props: {name: string;website?: string;}name: string;name: stringwebsite?: string }) =>website?: string | undefineddb.db: Database.Databaseadd(Database.add<BaseObj & Organization & OfKind<EntityKind.Object>>(obj: BaseObj & Organization & OfKind<EntityKind.Object>, opts?: Database.AddOptions): BaseObj & Organization & OfKind<EntityKind.Object>Adds an object or relation to the database.
Only Object and Relation entities are accepted. To persist a Type definition use
addType
— passing a Type entity is rejected at compile time (and at runtime).
Pass
{ to: feed }to append to a feed instead (synchronous; confirm withflush
).
Obj.import Objmake(function make<typeof Organization>(type: typeof Organization, props: NoInfer<Obj.MakeProps<typeof Organization>>, options?: Obj.MakeOptions): Obj.OfShape<Organization & OfKind<EntityKind.Object>>Creates a new echo object of the given schema or
Type.Type.Organization,class Organizationprops));props: {name: string;website?: string;}export constaddPerson = (const addPerson: (db: Database.Database, { employer, ...props }: {name: string;email?: string;employer?: Organization;}) => BaseObj & Person & OfKind<EntityKind.Object>db:db: Database.DatabaseDatabase.import DatabaseDatabase,interface DatabaseECHO Database interface.
{employer, ...employer: Organization | undefinedprops }: {props: {name: string;email?: string;}name: string;name: stringemail?: string;email?: string | undefinedemployer?:employer?: Organization | undefinedOrganization },class Organization) =>db.db: Database.Databaseadd(Database.add<BaseObj & Person & OfKind<EntityKind.Object>>(obj: BaseObj & Person & OfKind<EntityKind.Object>, opts?: Database.AddOptions): BaseObj & Person & OfKind<EntityKind.Object>Adds an object or relation to the database.
Only Object and Relation entities are accepted. To persist a Type definition use
addType
— passing a Type entity is rejected at compile time (and at runtime).
Pass
{ to: feed }to append to a feed instead (synchronous; confirm withflush
).
Obj.import Objmake(function make<typeof Person>(type: typeof Person, props: NoInfer<Obj.MakeProps<typeof Person>>, options?: Obj.MakeOptions): Obj.OfShape<Person & OfKind<EntityKind.Object>>Creates a new echo object of the given schema or
Type.Type.Person, { ...class Personprops, ...(props: {name: string;email?: string;}employer && {employer: Organization | undefinedemployer:employer?: Ref<Organization & OfKind<EntityKind.Object> & Obj.Unknown> | undefinedRef.import Refmake(const make: <Organization>(object: Organization) => Ref<Organization>Create a reference to an entity.
employer) }) }));employer: Organizationexport constaddEvent = (const addEvent: (db: Database.Database, { attendees, ...props }: {title: string;description?: string;attendees?: Person[];}) => BaseObj & Event & OfKind<EntityKind.Object>db:db: Database.DatabaseDatabase.import DatabaseDatabase,interface DatabaseECHO Database interface.
{attendees = [], ...attendees: Person[]props }: {props: {title: string;description?: string;}title: string;title: stringdescription?: string;description?: string | undefinedattendees?:attendees?: Person[] | undefinedPerson[] },class Person) =>db.db: Database.Databaseadd(Database.add<BaseObj & Event & OfKind<EntityKind.Object>>(obj: BaseObj & Event & OfKind<EntityKind.Object>, opts?: Database.AddOptions): BaseObj & Event & OfKind<EntityKind.Object>Adds an object or relation to the database.
Only Object and Relation entities are accepted. To persist a Type definition use
addType
— passing a Type entity is rejected at compile time (and at runtime).
Pass
{ to: feed }to append to a feed instead (synchronous; confirm withflush
).
Obj.import Objmake(function make<typeof Event>(type: typeof Event, props: NoInfer<Obj.MakeProps<typeof Event>>, options?: Obj.MakeOptions): Obj.OfShape<Event & OfKind<EntityKind.Object>>Creates a new echo object of the given schema or
Type.Type.Event, { ...class Eventprops,props: {title: string;description?: string;}attendees:attendees?: readonly Ref<Person & OfKind<EntityKind.Object> & Obj.Unknown>[] | undefinedattendees.attendees: Person[]map((Array<Person>.map<Ref<Person>>(callbackfn: (value: Person, index: number, array: Person[]) => Ref<Person>, thisArg?: any): Ref<Person>[]Calls a defined callback function on each element of an array, and returns an array that contains the results.
person) =>person: PersonRef.import Refmake(const make: <Person>(object: Person) => Ref<Person>Create a reference to an entity.
person)) }));person: PersonA person’s
employeris passed as an object and stored as a reference to it. -
Add records.
constnorthwind =const northwind: BaseObj & Organization & OfKind<EntityKind.Object>addOrganization(function addOrganization(db: Database.Database, props: {name: string;website?: string;}): BaseObj & Organization & OfKind<EntityKind.Object>db, {const db: Database.Databasename: 'Northwind Labs' });name: stringconstada =const ada: BaseObj & Person & OfKind<EntityKind.Object>addPerson(function addPerson(db: Database.Database, { employer, ...props }: {name: string;email?: string;employer?: Organization;}): BaseObj & Person & OfKind<EntityKind.Object>db, {const db: Database.Databasename: 'Ada Lovelace',name: stringemployer:employer?: Organization | undefinednorthwind });const northwind: BaseObj & Organization & OfKind<EntityKind.Object> -
Watch a record.
Obj.subscribecalls back after each change to the object, and returns a function that stops the subscription.constemployers: (string | undefined)[] = [];const employers: (string | undefined)[]constunsubscribe =const unsubscribe: () => voidObj.import Objsubscribe(const subscribe: (obj: Obj.Unknown, callback: () => void) => (() => void)Subscribe to object updates. The callback is called synchronously when the object is modified. Only accepts reactive objects (not snapshots).
ada, () => {const ada: BaseObj & Person & OfKind<EntityKind.Object>employers.const employers: (string | undefined)[]push(Array<string | undefined>.push(...items: (string | undefined)[]): numberAppends new elements to the end of an array, and returns the new length of the array.
ada.const ada: BaseObj & Person & OfKind<EntityKind.Object>employer?.employer?: Ref<Organization & OfKind<EntityKind.Object> & Obj.Unknown> | undefinedpeek()?.Ref<Organization & OfKind<EntityKind.Object> & Unknown>.peek(): (Organization & OfKind<EntityKind.Object> & Obj.Unknown) | undefinedname);name: string | undefined}); -
Change a record. The app moves a person to another organization in one update, and its form writes whatever fields the reader changed.
/** Moves a person to another organization. */export constchangeEmployer = (const changeEmployer: (person: Person, employer: Organization) => PersonMoves a person to another organization.
person:person: PersonPerson,class Personemployer:employer: OrganizationOrganization) =>class OrganizationObj.import Objupdate(const update: <Person>(obj: Person, callback: ChangeCallback<Person>) => PersonPerform mutations on an echo object within a controlled context.
All mutations within the callback are batched and trigger a single notification when the callback completes. Direct mutations outside of
Obj.updatewill throw an error for echo objects.This function also works with nested objects within echo objects (e.g., Template structs) that are reactive at runtime.
person, (person: Personperson) => {person: {name: string;email?: string | undefined;employer?: Ref<Organization & OfKind<EntityKind.Object> & Obj.Unknown> | undefined;id: EntityId;"~@dxos/echo/Kind": EntityKind.Object;}person.person: {name: string;email?: string | undefined;employer?: Ref<Organization & OfKind<EntityKind.Object> & Obj.Unknown> | undefined;id: EntityId;"~@dxos/echo/Kind": EntityKind.Object;}employer =employer?: Ref<Organization & OfKind<EntityKind.Object> & Obj.Unknown> | undefinedRef.import Refmake(const make: <Organization>(object: Organization) => Ref<Organization>Create a reference to an entity.
employer);employer: Organization});/** Writes the values a form returns onto a record, one field at a time. */export constupdateRecord = (const updateRecord: (record: Obj.Unknown, values: Record<string, unknown>) => Obj.UnknownWrites the values a form returns onto a record, one field at a time.
record:record: Obj.UnknownObj.import ObjUnknown,interface UnknownObject with no known properties beyond id and kind. Use this when the object's schema/properties are not known. For objects with arbitrary properties, use
Obj.AnyProps.NOTE: This is a TypeScript type only, not a schema. To validate that a value is an ECHO object, use
Schema.is(Type.Obj).Runtime Effect schema for any ECHO object. Use for validation, parsing, or as a reference target for collections.
NOTE:
Schema.is(Type.Obj)does STRUCTURAL validation only (checks foridfield). UseObj.isObject()for proper ECHO instance type guards that check the KindId brand.values:values: Record<string, unknown>Record<string, unknown>) =>type Record<K extends keyof any, T> = { [P in K]: T; }Construct a type with a set of properties K of type T
Obj.import Objupdate(const update: <Obj.Unknown>(obj: Obj.Unknown, callback: ChangeCallback<Obj.Unknown>) => Obj.UnknownPerform mutations on an echo object within a controlled context.
All mutations within the callback are batched and trigger a single notification when the callback completes. Direct mutations outside of
Obj.updatewill throw an error for echo objects.This function also works with nested objects within echo objects (e.g., Template structs) that are reactive at runtime.
record, (record: Obj.Unknownrecord) => {record: {id: EntityId;"~@dxos/echo/Kind": EntityKind.Object;}for (const [key,const key: stringvalue] ofconst value: unknownObject.var Object: ObjectConstructorProvides functionality common to all JavaScript objects.
entries(ObjectConstructor.entries<unknown>(o: {[s: string]: unknown;} | ArrayLike<unknown>): [string, unknown][] (+1 overload)Returns an array of key/values of the enumerable own properties of an object
values)) {values: Record<string, unknown>Obj.import ObjsetValue(const setValue: (obj: Obj.Mutable<Obj.Unknown>, path: readonly (string | number)[], value: any) => voidSet a deeply nested property on an object, using the object's schema to determine whether to initialize nested data as an empty object or array.
Similar to lodash.set and setDeep from
record, [record: {id: EntityId;"~@dxos/echo/Kind": EntityKind.Object;}key],const key: stringvalue);const value: unknown}});consthalcyon =const halcyon: BaseObj & Organization & OfKind<EntityKind.Object>addOrganization(function addOrganization(db: Database.Database, props: {name: string;website?: string;}): BaseObj & Organization & OfKind<EntityKind.Object>db, {const db: Database.Databasename: 'Halcyon Robotics' });name: stringchangeEmployer(function changeEmployer(person: Person, employer: Organization): PersonMoves a person to another organization.
ada,const ada: BaseObj & Person & OfKind<EntityKind.Object>halcyon);const halcyon: BaseObj & Organization & OfKind<EntityKind.Object>The subscriber from step 3 has run, so
employersholds'Halcyon Robotics'. -
Remove a record.
export constremoveRecord = (const removeRecord: (db: Database.Database, record: Obj.Unknown) => voiddb:db: Database.DatabaseDatabase.import DatabaseDatabase,interface DatabaseECHO Database interface.
record:record: Obj.UnknownObj.import ObjUnknown) =>interface UnknownObject with no known properties beyond id and kind. Use this when the object's schema/properties are not known. For objects with arbitrary properties, use
Obj.AnyProps.NOTE: This is a TypeScript type only, not a schema. To validate that a value is an ECHO object, use
Schema.is(Type.Obj).Runtime Effect schema for any ECHO object. Use for validation, parsing, or as a reference target for collections.
NOTE:
Schema.is(Type.Obj)does STRUCTURAL validation only (checks foridfield). UseObj.isObject()for proper ECHO instance type guards that check the KindId brand.db.db: Database.Databaseremove(Database.remove(obj: Unknown, opts?: Database.WriteOptions): voidRemoves object from the database.
record);record: Obj.UnknownremoveRecord(function removeRecord(db: Database.Database, record: Obj.Unknown): voiddb,const db: Database.Databaseada);const ada: BaseObj & Person & OfKind<EntityKind.Object>
Try it
Section titled “Try it”Select a record and change it in the form, or press New to add one of the chosen type. The × on a row removes it. A person’s employer is a picker of the organizations in the space.
What you can do now
Section titled “What you can do now”You can add records to a database, change them inside Obj.update, watch them change, and remove them.
Querying: list the records, sorted and searched.