{"id":42415252,"url":"https://github.com/lyonbot/linked-data","last_synced_at":"2026-01-28T01:58:50.431Z","repository":{"id":125270886,"uuid":"458730685","full_name":"lyonbot/linked-data","owner":"lyonbot","description":"Load and edit linked data easily","archived":false,"fork":false,"pushed_at":"2023-06-08T12:29:48.000Z","size":452,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-11T12:55:32.453Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lyonbot.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-02-13T06:35:00.000Z","updated_at":"2022-02-14T13:21:47.000Z","dependencies_parsed_at":null,"dependency_job_id":"2a2866a1-56b8-433c-aff8-a560a740c102","html_url":"https://github.com/lyonbot/linked-data","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/lyonbot/linked-data","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lyonbot%2Flinked-data","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lyonbot%2Flinked-data/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lyonbot%2Flinked-data/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lyonbot%2Flinked-data/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lyonbot","download_url":"https://codeload.github.com/lyonbot/linked-data/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lyonbot%2Flinked-data/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28833227,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-27T23:29:49.665Z","status":"ssl_error","status_checked_at":"2026-01-27T23:25:58.379Z","response_time":168,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-28T01:58:49.700Z","updated_at":"2026-01-28T01:58:50.425Z","avatar_url":"https://github.com/lyonbot.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @lyonbot/linked-data\n\nLoad edit linked (graph-like) data easily\n\n[ [Homepage](https://github.com/lyonbot/linked-data) | [GitHub](https://github.com/lyonbot/linked-data) | [NPM](https://www.npmjs.com/package/@lyonbot/linked-data) ]\n\n![](./images/concept.drawio.svg)\n\n## Usage\n\n```\nnpm i @lyonbot/linked-data\n```\n\n### Create Nodes\n\nLet's say we have such nested data:\n\n```js\nconst cardData = {\n  type: 'card',\n  theme: 'black',\n  children: [\n    { type: 'paragraph', children: ['Welcome'] },\n    { id: 'openBtn', type: 'button', children: ['Open'] },\n  ],\n};\n```\n\nAnd we know its pattern (schemas)\n\n```js\nconst schemas = {\n  Component: {\n    type: 'object',\n    key: 'id', // if exists, take `id` property as unique key\n    properties: {\n      children: 'ComponentArray', // Array also has its own schema (see below)\n    },\n\n  ComponentArray: {\n    type: 'array',\n    items: 'Component', // actual items can be non-object. we don't strictly validate the type\n  },\n};\n```\n\nWe can convert the data it into lots of connected nodes, following the schema relations.\n\n```js\nconst linkedData = new LinkedData({ schemas });\nconst cardNode = linkedData.import(cardData, 'Component');\n```\n\nThe `linkedData.import()` will follow \"Component\" schema, explode `cardData` to lots of DataNodes, make links between them, and return the entry DataNode -- the `cardNode` above.\n\n\u003e **Note:**\n\u003e\n\u003e **Schema does NOT validate value type**. A DataNode with \"object\" schema, can still storage anything -- array, string, number, etc.\n\u003e\n\u003e If actual type of `node.value` mismatches, the schema will be ignored temporarily, until value is set to correct type.\n\u003e\n\u003e In this example, some \"Component\" nodes store string values only. That's okay.\n\nEvery DataNode has a unique key. If you `import` twice without \"overwrite\" option, you will get new DataNodes. The new nodes will have different keys, although their contents are same as the old nodes'.\n\n![](./images/example1.drawio.svg)\n\n### Read, Edit, Link \u0026 Unlink\n\nDataNode provides [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy)-powered `node.value`. You can read, write, splice array, push elements with it freely.\n\nYou don't have to care about the links\n-- they are automatically converted into Proxy again. Just consume and mutate the value.\n\n```js\nconst card = cardNode.value;\n\n// you can read and write value to \"card\"\n\ncard.theme = 'light';\ncard.children.push('another text');\n\nconst button = card.children.find(child =\u003e child.id === 'openBtn');\nbutton.children.push({ type: 'icon', icon: 'caret-right' });\n\n// card is modified now\n```\n\nBecause we've prepared schemas for each DataNode, the `*.children.push` above, will automatically:\n\n1. create new \"Component\" DataNode\n2. modify the array, add new link\n\nIf you want to manually create a link, use `anotherNode.ref`:\n\n```js\n// create a DataNode with no Schema\nconst passwordNode = linkedData.import('dolphins');\n\n// make a link\ncardNode.value.password = passwordNode.ref;\n\n// read\nconsole.log(cardNode.value.password); // =\u003e \"dolphins\"\n\n// always synced\npasswordNode.value = 'nE7jA%5m';\nconsole.log(cardNode.value.password); // =\u003e \"nE7jA%5m\"\n\n// unlink\ncardNode.value.password = 'dead string';\nconsole.log(cardNode.value.password); // =\u003e \"dead string\"\nconsole.log(passwordNode.value); // =\u003e \"nE7jA%5m\" -- not affected\n```\n\n## Track Mutations, Undo \u0026 Redo\n\n\u003e This feature is tree-shakable\n\nWith **ModificationObserver** magic, all you need is:\n\n```js\nimport { ModificationObserver } from '@lyonbot/linked-data';\n\n// ... skip ...\n\nconst observer = new ModificationObserver(() =\u003e {\n  // find out what's changed\n  const records = observer.takeRecords();\n  console.log(`You just add / edit ${records.length} nodes!`);\n\n  // you can storage records to somewhere else.\n  // how to undo/redo? see the first figure above\n\n  // stop observing\n  observer.disconnect();\n});\n\n// start observing\nobserver.observeLinkedData(linkedData);\n\n// ----------------------------------------\n// now start to modify node.value\n// ...\n\nconst card = cardNode.value;\n\ncard.theme = 'light';\ncard.children.push('another text');\n\nconst button = card.children.find(child =\u003e child.id === 'openBtn');\nbutton.children.push({ type: 'icon', icon: 'caret-right' });\n```\n\nIn the `records`, you may get following deduced procedure:\n\n![](./images/example2.drawio.svg)\n\nWith the records, you can easily implement undo \u0026 redo:\n\n```js\nimport { applyPatches } from '@lyonbot/linked-data';\n\n// undo:\nrecords.forEach(record =\u003e {\n  record.node.value = applyPatches(record.node.value, record.revertPatches);\n});\n\n// then, redo:\nrecords.forEach(record =\u003e {\n  record.node.value = applyPatches(record.node.value, record.patches);\n});\n```\n\n## Theories\n\n### Mutable \u0026 Immutable\n\nIn the separated _Node_ list, every node is independent.\nIf you modify a _Node_, the other *Node*s referring it will NOT be mutated\n-- they only have a _reference_, not a _value_.\n\nTherefore, the nested data containing lots of *Node*s, is close to \"**mutable**\" philosophy.\n\nWe only cares about when a _Node_'s value changes. You can maintain in **mutable** way or **immutable** way\n-- it doesn't matter, as long as you can notify us that value is changed.\n\n💡 We suggest that maintain _Node value_ in **immutable** way.\nIt allows external libraries to utilize `Object.is(x, y)` and low-costly distinguish whether _value_ is really changed,\nwhere _value_ can be the whole Node or some property from Node.\n\n\u003cdetails\u003e\n\n\u003csummary\u003e💭 Some thoughts and facts\u003c/summary\u003e\n\n- To be aggressive, if we treat every object/array as _Node_ regardless of their semantic purposes,\n  we will get Vue or Mobx -- every non-primitive value can be \"observed\".\n\n- Web Component's attributes are always primitive data, which makes the comparison simple and low-cost.\n\n\u003c/details\u003e\n\n### Dependency Graph\n\nEvery Node can be referred.\n\nIt's easy to find out a Node's dependents with _linked-data_ because we collects necessary info while generating the separated Node list.\n\nThe dependency graph may be circular.\n\n### Identifier\n\nEvery Node needs an identifier.\n\n💡 Identifiers shall be **permanent, readonly, final** to a Node.\n\n💡 In a certain context, identifiers shall be **unique**.\n\nWe shall _always_ store it within Node's value. If a input Node has no identifier, we shall generated one, in current context.\n\nYou can see lots of generated, `unnamed_`-prefixed identifiers in the example above.\n\n\u003cdetails\u003e\n\n\u003csummary\u003e💭 Some thoughts and facts\u003c/summary\u003e\n\n- Vue doesn't need one because\n\n  1. Each object instance has a memory address in JavaScript engine.\n     We can use memory address as the identifier because Identifier's properties apply to memory addresses.\n\n  2. Vue doesn't hydrate two nested data.\n\n- MongoDB generates `_id` for each document.\n\n\u003c/details\u003e\n\n### When you need Schemas\n\nSchemas are optional.\n\n**Schema does NOT validate value type**. A DataNode with \"object\" schema, can still storage anything -- array, string, number, etc.\n\nIf actual type of `node.value` mismatches, the schema will be ignored temporarily, until value is set to correct type.\n\nWhat can a schema play a role in? You can define some rules by writing schemas:\n\n- `key`:\n\n  - when importing, how to read Identifier from raw JSON object\n  - when exporting, how to write Identifier to the exported JSON object\n\n- `properties` for objects, or `items` for arrays\n  - when importing, convert some properties it into a _Node Reference_.\n  - when exporting, convert some \"referring\" properties into node.\n  - when writing values (not reference) into certain properties, automatically create new Node and new reference.\n\nHowever the other properties **CAN** be a _Node Reference_ too -- user can make links anywhere.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flyonbot%2Flinked-data","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flyonbot%2Flinked-data","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flyonbot%2Flinked-data/lists"}