[
  {
    "path": ".editorconfig",
    "content": "# editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.md]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n*.log\nnode_modules\n.npmrc\npackage-lock.json\ndist\nbuild\n_private\n*.private.*\n"
  },
  {
    "path": ".npm-dev-registry.json",
    "content": "{\n  \"packages\": [\"./packages/*\"]\n}\n"
  },
  {
    "path": ".prettierignore",
    "content": "node_modules\ndist\nbuild\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\"esbenp.prettier-vscode\", \"editorconfig.editorconfig\"]\n}\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n- Demonstrating empathy and kindness toward other people\n- Being respectful of differing opinions, viewpoints, and experiences\n- Giving and gracefully accepting constructive feedback\n- Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n- Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n- The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n- Trolling, insulting or derogatory comments, and personal or political attacks\n- Public or private harassment\n- Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n- Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\nhello@layrjs.com.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior, harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Manuel Vila\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n\t<img src=\"assets/layr-logo-with-icon.svg\" width=\"200\" alt=\"Layr\">\n\t<br>\n\t<br>\n</p>\n\n> Dramatically simplify full‑stack development.\n\n## Documentation\n\nCheck out the [documentation](https://layrjs.com/docs) for a quick introduction and a comprehensive API description.\n\n## Roadmap\n\n### Components\n\n- [x] Basic components\n- [x] Controlled attributes\n- [x] Component provision\n- [x] Cross-layer inheritance\n- [x] Remote method invocation\n- [x] Optimized serialization\n- [ ] Weak Identity Map\n- [ ] Component subscriptions (realtime updates)\n- [ ] HTTP Caching\n\n### Storage\n\n- [x] Basic storage (MongoDB)\n- [x] Indexes\n  - [x] Identifier attributes\n  - [x] Regular attributes\n  - [x] Compound attributes\n  - [x] Referenced components\n  - [x] Embedded components\n- [ ] Automatic migrations\n  - [x] Indexes\n  - [ ] Default values\n  - [ ] Renamed components\n  - [ ] Renamed attributes\n- [ ] Custom migrations\n- [ ] Polymorphism\n- [ ] Transactions\n- [ ] Sugar to query reverse relationships\n- [ ] Query subscriptions (realtime updates)\n- [ ] Support for more databases (PostgreSQL, MySQL, DynamoDB,...)\n\n### Routing\n\n- [x] Basic routing\n- [x] Nested routing\n\n### Authorizations\n\n- [x] Basic authorizations\n- [x] Role-based authorizations\n\n### Integrations\n\n- [x] React integration\n- [x] Basic AWS integration\n\n## Contributing\n\nContributions are welcome.\n\nBefore contributing please read the [code of conduct](https://github.com/layrjs/layr/blob/master/CODE_OF_CONDUCT.md) and search the [issue tracker](https://github.com/layrjs/layr/issues) to find out if your issue has already been discussed before.\n\nTo contribute, [fork this repository](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo/), commit your changes, and [send a pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests).\n\n## License\n\nMIT\n"
  },
  {
    "path": "assets/frontend-backend.excalidraw",
    "content": "{\n  \"type\": \"excalidraw\",\n  \"version\": 2,\n  \"source\": \"https://excalidraw.com\",\n  \"elements\": [\n    {\n      \"id\": \"qmZPyH3vxEhjWDCfuvfXG\",\n      \"type\": \"rectangle\",\n      \"x\": 473.40625,\n      \"y\": -192.14453125,\n      \"width\": 859.4531250000005,\n      \"height\": 478.06640624999994,\n      \"angle\": 0,\n      \"strokeColor\": \"#eceff1\",\n      \"backgroundColor\": \"#263238\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"seed\": 1020737077,\n      \"version\": 490,\n      \"versionNonce\": 552337013,\n      \"isDeleted\": false,\n      \"boundElementIds\": null\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 868,\n      \"versionNonce\": 850464603,\n      \"isDeleted\": false,\n      \"id\": \"pZqWf7TeaoVvLCg8jN1fw\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 629.6852678571429,\n      \"y\": -62.34207589285717,\n      \"strokeColor\": \"#ff4081\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 110,\n      \"height\": 35,\n      \"seed\": 951595797,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Classes\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"id\": \"tzB0MO8qggda-Bt_Wm_TB\",\n      \"type\": \"ellipse\",\n      \"x\": 518.53125,\n      \"y\": -91.59375,\n      \"width\": 328.45703124999994,\n      \"height\": 328.45703124999994,\n      \"angle\": 0,\n      \"strokeColor\": \"#ff4081\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"seed\": 1145563989,\n      \"version\": 426,\n      \"versionNonce\": 1479436245,\n      \"isDeleted\": false,\n      \"boundElementIds\": [\n        \"mO83jV49wubJCif3eCFF7\"\n      ]\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 903,\n      \"versionNonce\": 1445751803,\n      \"isDeleted\": false,\n      \"id\": \"3XcH3jOWUovwndwQ5OWNZ\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 613.9001116071429,\n      \"y\": -8.635044642857167,\n      \"strokeColor\": \"#ff4081\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 135,\n      \"height\": 35,\n      \"seed\": 1510068853,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Instances\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 1064,\n      \"versionNonce\": 1109173557,\n      \"isDeleted\": false,\n      \"id\": \"jfBVn4_IY4-6kYgH4CLc2\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 541.4782366071429,\n      \"y\": 44.08370535714283,\n      \"strokeColor\": \"#ff4081\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 145,\n      \"height\": 35,\n      \"seed\": 1265264027,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Attributes\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 1171,\n      \"versionNonce\": 2001941659,\n      \"isDeleted\": false,\n      \"id\": \"pbft6g1zEaKzjxE9qI4WH\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 713.2165178571429,\n      \"y\": 43.12667410714283,\n      \"strokeColor\": \"#ff4081\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 109,\n      \"height\": 35,\n      \"seed\": 1694556846,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Methods\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 1101,\n      \"versionNonce\": 1109145237,\n      \"isDeleted\": false,\n      \"id\": \"Ml8ehnX8s0BLj1BCSxw5v\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 602.6696428571429,\n      \"y\": 101.35714285714283,\n      \"strokeColor\": \"#ff4081\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 148,\n      \"height\": 35,\n      \"seed\": 479631835,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"References\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 1129,\n      \"versionNonce\": 634906939,\n      \"isDeleted\": false,\n      \"id\": \"Lmij_syGf4Gxc0oIXhstt\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 608.6813616071429,\n      \"y\": 157.23995535714283,\n      \"strokeColor\": \"#ff4081\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 148,\n      \"height\": 35,\n      \"seed\": 622730715,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Exceptions\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 886,\n      \"versionNonce\": 853711861,\n      \"isDeleted\": false,\n      \"id\": \"g4v8TXu9aqJoc76AjvU5E\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 1073.730189732143,\n      \"y\": -65.31277901785715,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 110,\n      \"height\": 35,\n      \"seed\": 292264597,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Classes\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"id\": \"XS2Qi1--uR3zLQP7iXcp9\",\n      \"type\": \"ellipse\",\n      \"x\": 962.576171875,\n      \"y\": -92.56445312499999,\n      \"width\": 328.45703124999994,\n      \"height\": 328.45703124999994,\n      \"angle\": 0,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"seed\": 1084278075,\n      \"version\": 415,\n      \"versionNonce\": 1461520859,\n      \"isDeleted\": false,\n      \"boundElementIds\": null\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 923,\n      \"versionNonce\": 2088064341,\n      \"isDeleted\": false,\n      \"id\": \"2VgLJ7nXt0QOFYIBEIWAa\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 1057.945033482143,\n      \"y\": -13.605747767857153,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 135,\n      \"height\": 35,\n      \"seed\": 715489269,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Instances\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 1083,\n      \"versionNonce\": 808785531,\n      \"isDeleted\": false,\n      \"id\": \"rOfltch3LXDrC9hL2pwbP\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 985.3278459821429,\n      \"y\": 45.11300223214285,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 145,\n      \"height\": 35,\n      \"seed\": 1550476763,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Attributes\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 1190,\n      \"versionNonce\": 1781129909,\n      \"isDeleted\": false,\n      \"id\": \"woGF69RnnYqW_PttWeNeI\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 1157.066127232143,\n      \"y\": 44.15597098214285,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 109,\n      \"height\": 35,\n      \"seed\": 1111210325,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Methods\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 1116,\n      \"versionNonce\": 266506011,\n      \"isDeleted\": false,\n      \"id\": \"vJWV-kfqxj7Mk8eWoWdTi\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 1046.714564732143,\n      \"y\": 104.45284598214283,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 148,\n      \"height\": 35,\n      \"seed\": 1534521979,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"References\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 1141,\n      \"versionNonce\": 1084605461,\n      \"isDeleted\": false,\n      \"id\": \"puaE8IFTDuN6P4YNxaFcX\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 1052.726283482143,\n      \"y\": 164.26925223214283,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 148,\n      \"height\": 35,\n      \"seed\": 1904461493,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Exceptions\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"id\": \"6iVlZb2OSireaYoyJCTfe\",\n      \"type\": \"line\",\n      \"x\": 872.43359375,\n      \"y\": 74.57421875,\n      \"width\": 60.234375,\n      \"height\": 0.82421875,\n      \"angle\": 0,\n      \"strokeColor\": \"#eceff1\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"round\",\n      \"seed\": 298196123,\n      \"version\": 294,\n      \"versionNonce\": 1486353339,\n      \"isDeleted\": false,\n      \"boundElementIds\": null,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          60.234375,\n          -0.82421875\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": null,\n      \"endBinding\": null\n    },\n    {\n      \"id\": \"DKp1L9tGICjQBmD7WviKE\",\n      \"type\": \"line\",\n      \"x\": 924,\n      \"y\": 63.85156250000006,\n      \"width\": 14.00390625,\n      \"height\": 10.234375,\n      \"angle\": 0,\n      \"strokeColor\": \"#eceff1\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"round\",\n      \"seed\": 1899926395,\n      \"version\": 295,\n      \"versionNonce\": 333347189,\n      \"isDeleted\": false,\n      \"boundElementIds\": null,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          14.00390625,\n          10.234375\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": null,\n      \"endBinding\": null\n    },\n    {\n      \"id\": \"2KOhHgB2-vgVjCIrzDoFC\",\n      \"type\": \"line\",\n      \"x\": 938.40234375,\n      \"y\": 73.69140625,\n      \"width\": 19.35546875,\n      \"height\": 14.65234375,\n      \"angle\": 0,\n      \"strokeColor\": \"#eceff1\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"round\",\n      \"seed\": 1385976021,\n      \"version\": 289,\n      \"versionNonce\": 2054669403,\n      \"isDeleted\": false,\n      \"boundElementIds\": null,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          -19.35546875,\n          14.65234375\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": null,\n      \"endBinding\": null\n    },\n    {\n      \"id\": \"aMvlrbJ4JhavinX6IuIip\",\n      \"type\": \"line\",\n      \"x\": 871.75,\n      \"y\": 72.01171875,\n      \"width\": 15.390625,\n      \"height\": 8.05859375,\n      \"angle\": 0,\n      \"strokeColor\": \"#eceff1\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"round\",\n      \"seed\": 129732827,\n      \"version\": 331,\n      \"versionNonce\": 1561952981,\n      \"isDeleted\": false,\n      \"boundElementIds\": null,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          15.390625,\n          -8.05859375\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": null,\n      \"endBinding\": null\n    },\n    {\n      \"id\": \"Heg0L7qNczN3uKd2rUc4H\",\n      \"type\": \"line\",\n      \"x\": 869.2109375,\n      \"y\": 74.93359375,\n      \"width\": 20.64453125,\n      \"height\": 12.38671875,\n      \"angle\": 0,\n      \"strokeColor\": \"#eceff1\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"round\",\n      \"seed\": 592936059,\n      \"version\": 337,\n      \"versionNonce\": 1028948219,\n      \"isDeleted\": false,\n      \"boundElementIds\": null,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          20.64453125,\n          12.38671875\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": null,\n      \"endBinding\": null\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 1066,\n      \"versionNonce\": 1357668405,\n      \"isDeleted\": false,\n      \"id\": \"tGfMAIBrPStcxnXBLGc8q\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 605.6969866071429,\n      \"y\": -156.91238839285717,\n      \"strokeColor\": \"#eceff1\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 152,\n      \"height\": 45,\n      \"seed\": 704809819,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 36,\n      \"fontFamily\": 1,\n      \"text\": \"Frontend\",\n      \"baseline\": 32,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 1013,\n      \"versionNonce\": 1253091739,\n      \"isDeleted\": false,\n      \"id\": \"-eYtt3kyzRMtMkKUqbNJ1\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 1053.138392857143,\n      \"y\": -156.91238839285717,\n      \"strokeColor\": \"#eceff1\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 145,\n      \"height\": 45,\n      \"seed\": 553189045,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 36,\n      \"fontFamily\": 1,\n      \"text\": \"Backend\",\n      \"baseline\": 32,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    }\n  ],\n  \"appState\": {\n    \"viewBackgroundColor\": \"#000\",\n    \"gridSize\": null\n  }\n}"
  },
  {
    "path": "assets/frontend-webapi-backend.excalidraw",
    "content": "{\n  \"type\": \"excalidraw\",\n  \"version\": 2,\n  \"source\": \"https://excalidraw.com\",\n  \"elements\": [\n    {\n      \"id\": \"qmZPyH3vxEhjWDCfuvfXG\",\n      \"type\": \"rectangle\",\n      \"x\": 284.2421875,\n      \"y\": -189.6875,\n      \"width\": 1237.3945312500002,\n      \"height\": 478.06640624999994,\n      \"angle\": 0,\n      \"strokeColor\": \"#eceff1\",\n      \"backgroundColor\": \"#263238\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"seed\": 1020737077,\n      \"version\": 359,\n      \"versionNonce\": 815580763,\n      \"isDeleted\": false,\n      \"boundElementIds\": null\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 677,\n      \"versionNonce\": 1952969915,\n      \"isDeleted\": false,\n      \"id\": \"pZqWf7TeaoVvLCg8jN1fw\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 439.0641741071429,\n      \"y\": -59.88504464285717,\n      \"strokeColor\": \"#ff4081\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 110,\n      \"height\": 35,\n      \"seed\": 951595797,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Classes\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"id\": \"tzB0MO8qggda-Bt_Wm_TB\",\n      \"type\": \"ellipse\",\n      \"x\": 327.91015625,\n      \"y\": -89.13671875,\n      \"width\": 328.45703124999994,\n      \"height\": 328.45703124999994,\n      \"angle\": 0,\n      \"strokeColor\": \"#ff4081\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"seed\": 1145563989,\n      \"version\": 235,\n      \"versionNonce\": 768854261,\n      \"isDeleted\": false,\n      \"boundElementIds\": [\n        \"mO83jV49wubJCif3eCFF7\"\n      ]\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 712,\n      \"versionNonce\": 1555921717,\n      \"isDeleted\": false,\n      \"id\": \"3XcH3jOWUovwndwQ5OWNZ\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 423.2790178571429,\n      \"y\": -6.178013392857167,\n      \"strokeColor\": \"#ff4081\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 135,\n      \"height\": 35,\n      \"seed\": 1510068853,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Instances\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 873,\n      \"versionNonce\": 1655329909,\n      \"isDeleted\": false,\n      \"id\": \"jfBVn4_IY4-6kYgH4CLc2\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 350.8571428571429,\n      \"y\": 46.54073660714283,\n      \"strokeColor\": \"#ff4081\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 145,\n      \"height\": 35,\n      \"seed\": 1265264027,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Attributes\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 980,\n      \"versionNonce\": 858462555,\n      \"isDeleted\": false,\n      \"id\": \"pbft6g1zEaKzjxE9qI4WH\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 522.5954241071429,\n      \"y\": 45.58370535714283,\n      \"strokeColor\": \"#ff4081\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 109,\n      \"height\": 35,\n      \"seed\": 1694556846,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Methods\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 910,\n      \"versionNonce\": 2116520635,\n      \"isDeleted\": false,\n      \"id\": \"Ml8ehnX8s0BLj1BCSxw5v\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 412.0485491071429,\n      \"y\": 103.81417410714283,\n      \"strokeColor\": \"#ff4081\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 148,\n      \"height\": 35,\n      \"seed\": 479631835,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"References\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 938,\n      \"versionNonce\": 402422619,\n      \"isDeleted\": false,\n      \"id\": \"Lmij_syGf4Gxc0oIXhstt\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 418.0602678571429,\n      \"y\": 159.69698660714283,\n      \"strokeColor\": \"#ff4081\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 148,\n      \"height\": 35,\n      \"seed\": 622730715,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Exceptions\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 855,\n      \"versionNonce\": 776023291,\n      \"isDeleted\": false,\n      \"id\": \"g4v8TXu9aqJoc76AjvU5E\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 1262.507533482143,\n      \"y\": -62.85574776785715,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 110,\n      \"height\": 35,\n      \"seed\": 292264597,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Classes\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"id\": \"XS2Qi1--uR3zLQP7iXcp9\",\n      \"type\": \"ellipse\",\n      \"x\": 1151.353515625,\n      \"y\": -90.10742187499999,\n      \"width\": 328.45703124999994,\n      \"height\": 328.45703124999994,\n      \"angle\": 0,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"seed\": 1084278075,\n      \"version\": 384,\n      \"versionNonce\": 863007797,\n      \"isDeleted\": false,\n      \"boundElementIds\": null\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 892,\n      \"versionNonce\": 1361565083,\n      \"isDeleted\": false,\n      \"id\": \"2VgLJ7nXt0QOFYIBEIWAa\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 1246.722377232143,\n      \"y\": -11.148716517857153,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 135,\n      \"height\": 35,\n      \"seed\": 715489269,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Instances\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 1052,\n      \"versionNonce\": 291950453,\n      \"isDeleted\": false,\n      \"id\": \"rOfltch3LXDrC9hL2pwbP\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 1174.105189732143,\n      \"y\": 47.57003348214285,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 145,\n      \"height\": 35,\n      \"seed\": 1550476763,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Attributes\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 1159,\n      \"versionNonce\": 952618587,\n      \"isDeleted\": false,\n      \"id\": \"woGF69RnnYqW_PttWeNeI\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 1345.843470982143,\n      \"y\": 46.61300223214285,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 109,\n      \"height\": 35,\n      \"seed\": 1111210325,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Methods\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 1085,\n      \"versionNonce\": 785296117,\n      \"isDeleted\": false,\n      \"id\": \"vJWV-kfqxj7Mk8eWoWdTi\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 1235.491908482143,\n      \"y\": 106.90987723214283,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 148,\n      \"height\": 35,\n      \"seed\": 1534521979,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"References\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 1110,\n      \"versionNonce\": 1794992859,\n      \"isDeleted\": false,\n      \"id\": \"puaE8IFTDuN6P4YNxaFcX\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 1241.503627232143,\n      \"y\": 166.72628348214283,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 148,\n      \"height\": 35,\n      \"seed\": 1904461493,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Exceptions\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"id\": \"KHG3bd42lmlGY_6htw4yh\",\n      \"type\": \"diamond\",\n      \"x\": 767.68359375,\n      \"y\": -87.37890625,\n      \"width\": 276.44140625000006,\n      \"height\": 326.80859375,\n      \"angle\": 0,\n      \"strokeColor\": \"#aeea00\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"seed\": 1156814363,\n      \"version\": 679,\n      \"versionNonce\": 158888565,\n      \"isDeleted\": false,\n      \"boundElementIds\": null\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 828,\n      \"versionNonce\": 816995989,\n      \"isDeleted\": false,\n      \"id\": \"PTJtBBiRYU7sirmpKXaaa\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 876.2438616071429,\n      \"y\": -29.291294642857167,\n      \"strokeColor\": \"#aeea00\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 53,\n      \"height\": 35,\n      \"seed\": 1015869717,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Get\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 896,\n      \"versionNonce\": 823673147,\n      \"isDeleted\": false,\n      \"id\": \"Ui7ygay9V1Gwb_JPUnSoZ\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 835.4743303571429,\n      \"y\": 21.189174107142833,\n      \"strokeColor\": \"#aeea00\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 64,\n      \"height\": 35,\n      \"seed\": 2081802805,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Post\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 1114,\n      \"versionNonce\": 1014219765,\n      \"isDeleted\": false,\n      \"id\": \"mCXJ_OIM1l6uuO5aerbPl\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 808.4547991071429,\n      \"y\": 74.62667410714283,\n      \"strokeColor\": \"#aeea00\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 96,\n      \"height\": 35,\n      \"seed\": 872123483,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Delete\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 950,\n      \"versionNonce\": 1217552859,\n      \"isDeleted\": false,\n      \"id\": \"PvsMVeFX5yNreGLLR6cZl\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 929.6696428571429,\n      \"y\": 23.579799107142833,\n      \"strokeColor\": \"#aeea00\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 48,\n      \"height\": 35,\n      \"seed\": 89552757,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Put\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 945,\n      \"versionNonce\": 161578325,\n      \"isDeleted\": false,\n      \"id\": \"wWagJGH0s7sy5ZggcAPSU\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 931.4508928571429,\n      \"y\": 75.54464285714283,\n      \"strokeColor\": \"#aeea00\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 67,\n      \"height\": 35,\n      \"seed\": 1694554683,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"URLs\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 978,\n      \"versionNonce\": 1301642165,\n      \"isDeleted\": false,\n      \"id\": \"5uBcTnXeNlcF037DuFLt8\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 864.4860491071429,\n      \"y\": 130.73604910714283,\n      \"strokeColor\": \"#aeea00\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 76,\n      \"height\": 35,\n      \"seed\": 989301691,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"JSON\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"id\": \"6iVlZb2OSireaYoyJCTfe\",\n      \"type\": \"line\",\n      \"x\": 681.8125,\n      \"y\": 77.03125,\n      \"width\": 60.234375,\n      \"height\": 0.82421875,\n      \"angle\": 0,\n      \"strokeColor\": \"#eceff1\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"round\",\n      \"seed\": 298196123,\n      \"version\": 103,\n      \"versionNonce\": 1469817205,\n      \"isDeleted\": false,\n      \"boundElementIds\": null,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          60.234375,\n          -0.82421875\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": null,\n      \"endBinding\": null\n    },\n    {\n      \"id\": \"DKp1L9tGICjQBmD7WviKE\",\n      \"type\": \"line\",\n      \"x\": 733.37890625,\n      \"y\": 66.30859375000006,\n      \"width\": 14.00390625,\n      \"height\": 10.234375,\n      \"angle\": 0,\n      \"strokeColor\": \"#eceff1\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"round\",\n      \"seed\": 1899926395,\n      \"version\": 104,\n      \"versionNonce\": 981539931,\n      \"isDeleted\": false,\n      \"boundElementIds\": null,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          14.00390625,\n          10.234375\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": null,\n      \"endBinding\": null\n    },\n    {\n      \"id\": \"2KOhHgB2-vgVjCIrzDoFC\",\n      \"type\": \"line\",\n      \"x\": 747.78125,\n      \"y\": 76.1484375,\n      \"width\": 19.35546875,\n      \"height\": 14.65234375,\n      \"angle\": 0,\n      \"strokeColor\": \"#eceff1\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"round\",\n      \"seed\": 1385976021,\n      \"version\": 98,\n      \"versionNonce\": 2129767125,\n      \"isDeleted\": false,\n      \"boundElementIds\": null,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          -19.35546875,\n          14.65234375\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": null,\n      \"endBinding\": null\n    },\n    {\n      \"id\": \"aMvlrbJ4JhavinX6IuIip\",\n      \"type\": \"line\",\n      \"x\": 681.12890625,\n      \"y\": 74.46875,\n      \"width\": 15.390625,\n      \"height\": 8.05859375,\n      \"angle\": 0,\n      \"strokeColor\": \"#eceff1\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"round\",\n      \"seed\": 129732827,\n      \"version\": 140,\n      \"versionNonce\": 1651693819,\n      \"isDeleted\": false,\n      \"boundElementIds\": null,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          15.390625,\n          -8.05859375\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": null,\n      \"endBinding\": null\n    },\n    {\n      \"id\": \"Heg0L7qNczN3uKd2rUc4H\",\n      \"type\": \"line\",\n      \"x\": 678.58984375,\n      \"y\": 77.390625,\n      \"width\": 20.64453125,\n      \"height\": 12.38671875,\n      \"angle\": 0,\n      \"strokeColor\": \"#eceff1\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"round\",\n      \"seed\": 592936059,\n      \"version\": 146,\n      \"versionNonce\": 1434421301,\n      \"isDeleted\": false,\n      \"boundElementIds\": null,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          20.64453125,\n          12.38671875\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": null,\n      \"endBinding\": null\n    },\n    {\n      \"id\": \"MziFM0bqQkfptvWC6js9V\",\n      \"type\": \"line\",\n      \"x\": 1065.697265625,\n      \"y\": 81.41796874999997,\n      \"width\": 60.234375,\n      \"height\": 0.82421875,\n      \"angle\": 0,\n      \"strokeColor\": \"#eceff1\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"round\",\n      \"seed\": 194372437,\n      \"version\": 241,\n      \"versionNonce\": 1782841755,\n      \"isDeleted\": false,\n      \"boundElementIds\": null,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          60.234375,\n          -0.82421875\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": null,\n      \"endBinding\": null\n    },\n    {\n      \"id\": \"1K59ruDxOFEs38bKS3wOR\",\n      \"type\": \"line\",\n      \"x\": 1117.263671875,\n      \"y\": 70.69531250000003,\n      \"width\": 14.00390625,\n      \"height\": 10.234375,\n      \"angle\": 0,\n      \"strokeColor\": \"#eceff1\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"round\",\n      \"seed\": 195848315,\n      \"version\": 248,\n      \"versionNonce\": 1294645653,\n      \"isDeleted\": false,\n      \"boundElementIds\": null,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          14.00390625,\n          10.234375\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": null,\n      \"endBinding\": null\n    },\n    {\n      \"id\": \"HsDXwOGJ1sjxaRJXDDZvp\",\n      \"type\": \"line\",\n      \"x\": 1131.666015625,\n      \"y\": 80.53515624999997,\n      \"width\": 19.35546875,\n      \"height\": 14.65234375,\n      \"angle\": 0,\n      \"strokeColor\": \"#eceff1\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"round\",\n      \"seed\": 570994869,\n      \"version\": 236,\n      \"versionNonce\": 2066159163,\n      \"isDeleted\": false,\n      \"boundElementIds\": null,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          -19.35546875,\n          14.65234375\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": null,\n      \"endBinding\": null\n    },\n    {\n      \"id\": \"0pX2lORYW_GourTAVS2dY\",\n      \"type\": \"line\",\n      \"x\": 1065.013671875,\n      \"y\": 78.85546874999997,\n      \"width\": 15.390625,\n      \"height\": 8.05859375,\n      \"angle\": 0,\n      \"strokeColor\": \"#eceff1\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"round\",\n      \"seed\": 1688183067,\n      \"version\": 278,\n      \"versionNonce\": 2133376757,\n      \"isDeleted\": false,\n      \"boundElementIds\": null,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          15.390625,\n          -8.05859375\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": null,\n      \"endBinding\": null\n    },\n    {\n      \"id\": \"V7OyGddYwmngOBKRWiOir\",\n      \"type\": \"line\",\n      \"x\": 1062.474609375,\n      \"y\": 81.77734374999997,\n      \"width\": 20.64453125,\n      \"height\": 12.38671875,\n      \"angle\": 0,\n      \"strokeColor\": \"#eceff1\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"round\",\n      \"seed\": 1870320149,\n      \"version\": 284,\n      \"versionNonce\": 1946064603,\n      \"isDeleted\": false,\n      \"boundElementIds\": null,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          20.64453125,\n          12.38671875\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": null,\n      \"endBinding\": null\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 875,\n      \"versionNonce\": 1438821333,\n      \"isDeleted\": false,\n      \"id\": \"tGfMAIBrPStcxnXBLGc8q\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 415.0758928571429,\n      \"y\": -154.45535714285717,\n      \"strokeColor\": \"#eceff1\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 152,\n      \"height\": 45,\n      \"seed\": 704809819,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 36,\n      \"fontFamily\": 1,\n      \"text\": \"Frontend\",\n      \"baseline\": 32,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 932,\n      \"versionNonce\": 1294663739,\n      \"isDeleted\": false,\n      \"id\": \"D4N_3rWoMa0_PDwOF4QG4\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 829.7399553571429,\n      \"y\": -150.45535714285717,\n      \"strokeColor\": \"#eceff1\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 149,\n      \"height\": 45,\n      \"seed\": 1200951285,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 36,\n      \"fontFamily\": 1,\n      \"text\": \"Web API\",\n      \"baseline\": 32,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 982,\n      \"versionNonce\": 1744068155,\n      \"isDeleted\": false,\n      \"id\": \"-eYtt3kyzRMtMkKUqbNJ1\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 1241.915736607143,\n      \"y\": -154.45535714285717,\n      \"strokeColor\": \"#eceff1\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 145,\n      \"height\": 45,\n      \"seed\": 553189045,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 36,\n      \"fontFamily\": 1,\n      \"text\": \"Backend\",\n      \"baseline\": 32,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    }\n  ],\n  \"appState\": {\n    \"viewBackgroundColor\": \"#000\",\n    \"gridSize\": null\n  }\n}"
  },
  {
    "path": "assets/typical-stack-vs-layr-stack-light-mode.excalidraw",
    "content": "{\n  \"type\": \"excalidraw\",\n  \"version\": 2,\n  \"source\": \"https://excalidraw.com\",\n  \"elements\": [\n    {\n      \"type\": \"rectangle\",\n      \"version\": 518,\n      \"versionNonce\": 1142497202,\n      \"isDeleted\": false,\n      \"id\": \"78pcPSx_TrkBrqMvVMUaS\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 314.42578125,\n      \"y\": -47.8984375,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 250.83482142857142,\n      \"height\": 67.28515625,\n      \"seed\": 132882798,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": []\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 203,\n      \"versionNonce\": 2102357102,\n      \"isDeleted\": false,\n      \"id\": \"Tg35xQQF7BB9-_T5IlAPi\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 345.87890625,\n      \"y\": -31.755859375,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 187,\n      \"height\": 35,\n      \"seed\": 74600110,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Data Access\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"rectangle\",\n      \"version\": 722,\n      \"versionNonce\": 1392400754,\n      \"isDeleted\": false,\n      \"id\": \"isBhZDRkcIoWGbySBsLEy\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 312.97265625,\n      \"y\": 51.353515625,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 252.4587053571429,\n      \"height\": 67.28515625,\n      \"seed\": 298779118,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": []\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 403,\n      \"versionNonce\": 633581230,\n      \"isDeleted\": false,\n      \"id\": \"j7kpflnVnuaDP-1F5MCcc\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 340.42578125,\n      \"y\": 67.49609375,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 199,\n      \"height\": 35,\n      \"seed\": 11834414,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Backend Model\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"rectangle\",\n      \"version\": 812,\n      \"versionNonce\": 1284922162,\n      \"isDeleted\": false,\n      \"id\": \"zk1Tzfs7TN7NuvV9nUrkK\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 312.97265625,\n      \"y\": 150.568359375,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 253.4743303571429,\n      \"height\": 67.28515625,\n      \"seed\": 1135815662,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [\n        \"_q_pfShbWagNPeqr2Cw4q\",\n        \"CzbKjuh5LlBF8vEsdGoYs\",\n        \"euFzlspcyBdei7ZjDGsTE\"\n      ]\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 429,\n      \"versionNonce\": 854960366,\n      \"isDeleted\": false,\n      \"id\": \"pbft6g1zEaKzjxE9qI4WH\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 366.1735491071429,\n      \"y\": 166.48214285714283,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 152,\n      \"height\": 35,\n      \"seed\": 1694556846,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"API Server\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"rectangle\",\n      \"version\": 837,\n      \"versionNonce\": 1598563570,\n      \"isDeleted\": false,\n      \"id\": \"G_sH-_BODgjExxHFIJUO7\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 313.0546875,\n      \"y\": 251.623046875,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 251.515625,\n      \"height\": 67.28515625,\n      \"seed\": 2087476462,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [\n        \"P-5Se_P31c9YU_W_jiyRL\",\n        \"CzbKjuh5LlBF8vEsdGoYs\"\n      ]\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 467,\n      \"versionNonce\": 879647534,\n      \"isDeleted\": false,\n      \"id\": \"FWR6GQyMu_VgSTbgtdktG\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 369.21205357142856,\n      \"y\": 268.20647321428567,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 144,\n      \"height\": 35,\n      \"seed\": 82743086,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"API Client\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"rectangle\",\n      \"version\": 813,\n      \"versionNonce\": 1419425458,\n      \"isDeleted\": false,\n      \"id\": \"RxupO9IzGSd0WgL0ECubS\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 312.97265625,\n      \"y\": 355.587890625,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 253.07812500000003,\n      \"height\": 67.28515625,\n      \"seed\": 290532466,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [\n        \"GNYre13yKH_YUl3IHWLJV\",\n        \"f72O2kwJb9RVWZ6WUwfn-\",\n        \"tcnX3h7U3Y7nCrYotIHRV\",\n        \"P-5Se_P31c9YU_W_jiyRL\"\n      ]\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 516,\n      \"versionNonce\": 1620493678,\n      \"isDeleted\": false,\n      \"id\": \"UJ8QgwFB2cterDA5_P1XR\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 337.83761160714283,\n      \"y\": 370.9436383928572,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 205,\n      \"height\": 35,\n      \"seed\": 2132201010,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [\n        \"uGtob-oyCnRKt3cM6Pyg_\"\n      ],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Frontend Model\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"rectangle\",\n      \"version\": 900,\n      \"versionNonce\": 758452338,\n      \"isDeleted\": false,\n      \"id\": \"I5x5Y1aUFonRrUCGi6_jw\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 312.97265625,\n      \"y\": 458.373046875,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 252.57421875000006,\n      \"height\": 67.28515625,\n      \"seed\": 1935830894,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [\n        \"GNYre13yKH_YUl3IHWLJV\",\n        \"f72O2kwJb9RVWZ6WUwfn-\",\n        \"qDACPeHQipsh0zAP-atrl\"\n      ]\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 591,\n      \"versionNonce\": 951576494,\n      \"isDeleted\": false,\n      \"id\": \"Ozy5i1Z2EEzX3kAVJ5IY9\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 335.1579241071429,\n      \"y\": 474.50446428571445,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 207,\n      \"height\": 35,\n      \"seed\": 1037108850,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [\n        \"uGtob-oyCnRKt3cM6Pyg_\"\n      ],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"User Interface\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"rectangle\",\n      \"version\": 1078,\n      \"versionNonce\": 1083733508,\n      \"isDeleted\": false,\n      \"id\": \"BK5gVfRenINsXjknIgSxf\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"dotted\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 695.0412946428572,\n      \"y\": 153.30106026785717,\n      \"strokeColor\": \"#ff4081\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 195.533482142857,\n      \"height\": 67.28515625,\n      \"seed\": 544079922,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [\n        \"xQNTkIrt5bvAXhVmKvBxM\",\n        \"hTkLQGX8fKKkms2V_vm0r\",\n        \"CzbKjuh5LlBF8vEsdGoYs\",\n        \"mnKhSKUhk7EFCekLtz1fQ\"\n      ]\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 787,\n      \"versionNonce\": 198499388,\n      \"isDeleted\": false,\n      \"id\": \"iQoD1G_MsPZm4xfuufUxe\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 734.5055803571429,\n      \"y\": 168.5563616071429,\n      \"strokeColor\": \"#ff4081\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 113,\n      \"height\": 35,\n      \"seed\": 48441842,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Backend\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"rectangle\",\n      \"version\": 1220,\n      \"versionNonce\": 1839404420,\n      \"isDeleted\": false,\n      \"id\": \"pCkj2Qaeh9PDMAzSCWPM9\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"dotted\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 694.9520089285714,\n      \"y\": 252.49637276785717,\n      \"strokeColor\": \"#ff4081\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 193.7477678571429,\n      \"height\": 67.28515625,\n      \"seed\": 374338158,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [\n        \"xQNTkIrt5bvAXhVmKvBxM\",\n        \"hTkLQGX8fKKkms2V_vm0r\",\n        \"mnKhSKUhk7EFCekLtz1fQ\"\n      ]\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 810,\n      \"versionNonce\": 31847612,\n      \"isDeleted\": false,\n      \"id\": \"-CvZWfngqmHMBLZxpCqri\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 732.0145089285714,\n      \"y\": 269.67131696428584,\n      \"strokeColor\": \"#ff4081\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 119,\n      \"height\": 35,\n      \"seed\": 415462574,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Frontend\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"rectangle\",\n      \"version\": 1501,\n      \"versionNonce\": 1865270532,\n      \"isDeleted\": false,\n      \"id\": \"MSfRFHL5NGAG-wZc7hZub\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 664.8816964285714,\n      \"y\": 117.36356026785717,\n      \"strokeColor\": \"#ff4081\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 252.3643973214285,\n      \"height\": 231.47656249999994,\n      \"seed\": 153297006,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [\n        \"CzbKjuh5LlBF8vEsdGoYs\",\n        \"euFzlspcyBdei7ZjDGsTE\"\n      ]\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 347,\n      \"versionNonce\": 1710969860,\n      \"isDeleted\": false,\n      \"id\": \"HblX02yOmNDLcQXmIx14_\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 318.7762276785714,\n      \"y\": -124.638671875,\n      \"strokeColor\": \"#607d8b\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 244,\n      \"height\": 45,\n      \"seed\": 1398208946,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 36,\n      \"fontFamily\": 1,\n      \"text\": \"Typical Stack\",\n      \"baseline\": 32,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 666,\n      \"versionNonce\": 152493628,\n      \"isDeleted\": false,\n      \"id\": \"oi9Jo0SrqAcTWeuWmDnFT\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 670.4157366071428,\n      \"y\": 42.34793526785717,\n      \"strokeColor\": \"#607d8b\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 241,\n      \"height\": 45,\n      \"seed\": 1175668722,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 36,\n      \"fontFamily\": 1,\n      \"text\": \"Layr Stack\",\n      \"baseline\": 32,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"rectangle\",\n      \"version\": 864,\n      \"versionNonce\": 459154052,\n      \"isDeleted\": false,\n      \"id\": \"snrOZbuQdOfsbwfDVsvOh\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 259.15066964285734,\n      \"y\": -163.32142857142867,\n      \"strokeColor\": \"#607d8b\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 711.9587053571425,\n      \"height\": 743.0351562500001,\n      \"seed\": 2001414322,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": []\n    },\n    {\n      \"type\": \"arrow\",\n      \"version\": 746,\n      \"versionNonce\": 926940476,\n      \"isDeleted\": false,\n      \"id\": \"mnKhSKUhk7EFCekLtz1fQ\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 1,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 795.6113691236103,\n      \"y\": 251.49637276785717,\n      \"strokeColor\": \"#ff4081\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 0.5444303450863117,\n      \"height\": 29.553292410714278,\n      \"seed\": 1730055982,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"round\",\n      \"boundElementIds\": [],\n      \"startBinding\": {\n        \"elementId\": \"pCkj2Qaeh9PDMAzSCWPM9\",\n        \"focus\": 0.032282012887604794,\n        \"gap\": 1\n      },\n      \"endBinding\": {\n        \"elementId\": \"BK5gVfRenINsXjknIgSxf\",\n        \"focus\": -0.040579986296433085,\n        \"gap\": 1.3568638392857224\n      },\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          0.5444303450863117,\n          -29.553292410714278\n        ]\n      ],\n      \"lastCommittedPoint\": null\n    },\n    {\n      \"type\": \"arrow\",\n      \"version\": 55,\n      \"versionNonce\": 1268233916,\n      \"isDeleted\": false,\n      \"id\": \"euFzlspcyBdei7ZjDGsTE\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 583.1283482142857,\n      \"y\": 234.59709821428572,\n      \"strokeColor\": \"#607d8b\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 62.05624021153369,\n      \"height\": 0.7102786712734144,\n      \"seed\": 1376797106,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"round\",\n      \"boundElementIds\": [],\n      \"startBinding\": {\n        \"elementId\": \"zk1Tzfs7TN7NuvV9nUrkK\",\n        \"focus\": 1.4825585590033774,\n        \"gap\": 16.743582589285722\n      },\n      \"endBinding\": {\n        \"elementId\": \"MSfRFHL5NGAG-wZc7hZub\",\n        \"focus\": 0.0075499005148347426,\n        \"gap\": 19.697108002752145\n      },\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          62.05624021153369,\n          -0.7102786712734144\n        ]\n      ],\n      \"lastCommittedPoint\": null\n    }\n  ],\n  \"appState\": {\n    \"viewBackgroundColor\": \"#ffffff\",\n    \"gridSize\": null\n  }\n}\n"
  },
  {
    "path": "assets/typical-stack-vs-layr-stack.excalidraw",
    "content": "{\n  \"type\": \"excalidraw\",\n  \"version\": 2,\n  \"source\": \"https://excalidraw.com\",\n  \"elements\": [\n    {\n      \"type\": \"rectangle\",\n      \"version\": 518,\n      \"versionNonce\": 1142497202,\n      \"isDeleted\": false,\n      \"id\": \"78pcPSx_TrkBrqMvVMUaS\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 314.42578125,\n      \"y\": -47.8984375,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 250.83482142857142,\n      \"height\": 67.28515625,\n      \"seed\": 132882798,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": []\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 203,\n      \"versionNonce\": 2102357102,\n      \"isDeleted\": false,\n      \"id\": \"Tg35xQQF7BB9-_T5IlAPi\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 345.87890625,\n      \"y\": -31.755859375,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 187,\n      \"height\": 35,\n      \"seed\": 74600110,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Data Access\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"rectangle\",\n      \"version\": 722,\n      \"versionNonce\": 1392400754,\n      \"isDeleted\": false,\n      \"id\": \"isBhZDRkcIoWGbySBsLEy\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 312.97265625,\n      \"y\": 51.353515625,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 252.4587053571429,\n      \"height\": 67.28515625,\n      \"seed\": 298779118,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": []\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 403,\n      \"versionNonce\": 633581230,\n      \"isDeleted\": false,\n      \"id\": \"j7kpflnVnuaDP-1F5MCcc\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 340.42578125,\n      \"y\": 67.49609375,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 199,\n      \"height\": 35,\n      \"seed\": 11834414,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Backend Model\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"rectangle\",\n      \"version\": 812,\n      \"versionNonce\": 1284922162,\n      \"isDeleted\": false,\n      \"id\": \"zk1Tzfs7TN7NuvV9nUrkK\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 312.97265625,\n      \"y\": 150.568359375,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 253.4743303571429,\n      \"height\": 67.28515625,\n      \"seed\": 1135815662,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [\n        \"_q_pfShbWagNPeqr2Cw4q\",\n        \"CzbKjuh5LlBF8vEsdGoYs\",\n        \"euFzlspcyBdei7ZjDGsTE\"\n      ]\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 429,\n      \"versionNonce\": 854960366,\n      \"isDeleted\": false,\n      \"id\": \"pbft6g1zEaKzjxE9qI4WH\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 366.1735491071429,\n      \"y\": 166.48214285714283,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 152,\n      \"height\": 35,\n      \"seed\": 1694556846,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"API Server\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"rectangle\",\n      \"version\": 837,\n      \"versionNonce\": 1598563570,\n      \"isDeleted\": false,\n      \"id\": \"G_sH-_BODgjExxHFIJUO7\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 313.0546875,\n      \"y\": 251.623046875,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 251.515625,\n      \"height\": 67.28515625,\n      \"seed\": 2087476462,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [\n        \"P-5Se_P31c9YU_W_jiyRL\",\n        \"CzbKjuh5LlBF8vEsdGoYs\"\n      ]\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 467,\n      \"versionNonce\": 879647534,\n      \"isDeleted\": false,\n      \"id\": \"FWR6GQyMu_VgSTbgtdktG\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 369.21205357142856,\n      \"y\": 268.20647321428567,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 144,\n      \"height\": 35,\n      \"seed\": 82743086,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"API Client\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"rectangle\",\n      \"version\": 813,\n      \"versionNonce\": 1419425458,\n      \"isDeleted\": false,\n      \"id\": \"RxupO9IzGSd0WgL0ECubS\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 312.97265625,\n      \"y\": 355.587890625,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 253.07812500000003,\n      \"height\": 67.28515625,\n      \"seed\": 290532466,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [\n        \"GNYre13yKH_YUl3IHWLJV\",\n        \"f72O2kwJb9RVWZ6WUwfn-\",\n        \"tcnX3h7U3Y7nCrYotIHRV\",\n        \"P-5Se_P31c9YU_W_jiyRL\"\n      ]\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 516,\n      \"versionNonce\": 1620493678,\n      \"isDeleted\": false,\n      \"id\": \"UJ8QgwFB2cterDA5_P1XR\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 337.83761160714283,\n      \"y\": 370.9436383928572,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 205,\n      \"height\": 35,\n      \"seed\": 2132201010,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [\n        \"uGtob-oyCnRKt3cM6Pyg_\"\n      ],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Frontend Model\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"rectangle\",\n      \"version\": 900,\n      \"versionNonce\": 758452338,\n      \"isDeleted\": false,\n      \"id\": \"I5x5Y1aUFonRrUCGi6_jw\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 312.97265625,\n      \"y\": 458.373046875,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 252.57421875000006,\n      \"height\": 67.28515625,\n      \"seed\": 1935830894,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [\n        \"GNYre13yKH_YUl3IHWLJV\",\n        \"f72O2kwJb9RVWZ6WUwfn-\",\n        \"qDACPeHQipsh0zAP-atrl\"\n      ]\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 591,\n      \"versionNonce\": 951576494,\n      \"isDeleted\": false,\n      \"id\": \"Ozy5i1Z2EEzX3kAVJ5IY9\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 335.1579241071429,\n      \"y\": 474.50446428571445,\n      \"strokeColor\": \"#03a9f4\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 207,\n      \"height\": 35,\n      \"seed\": 1037108850,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [\n        \"uGtob-oyCnRKt3cM6Pyg_\"\n      ],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"User Interface\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"rectangle\",\n      \"version\": 1077,\n      \"versionNonce\": 1021194802,\n      \"isDeleted\": false,\n      \"id\": \"BK5gVfRenINsXjknIgSxf\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"dotted\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 695.0412946428572,\n      \"y\": 153.30106026785717,\n      \"strokeColor\": \"#aeea00\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 195.533482142857,\n      \"height\": 67.28515625,\n      \"seed\": 544079922,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [\n        \"xQNTkIrt5bvAXhVmKvBxM\",\n        \"hTkLQGX8fKKkms2V_vm0r\",\n        \"CzbKjuh5LlBF8vEsdGoYs\",\n        \"mnKhSKUhk7EFCekLtz1fQ\"\n      ]\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 786,\n      \"versionNonce\": 754631150,\n      \"isDeleted\": false,\n      \"id\": \"iQoD1G_MsPZm4xfuufUxe\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 734.5055803571429,\n      \"y\": 168.5563616071429,\n      \"strokeColor\": \"#aeea00\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 113,\n      \"height\": 35,\n      \"seed\": 48441842,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Backend\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"rectangle\",\n      \"version\": 1219,\n      \"versionNonce\": 1182604274,\n      \"isDeleted\": false,\n      \"id\": \"pCkj2Qaeh9PDMAzSCWPM9\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"dotted\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 694.9520089285714,\n      \"y\": 252.49637276785717,\n      \"strokeColor\": \"#aeea00\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 193.7477678571429,\n      \"height\": 67.28515625,\n      \"seed\": 374338158,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [\n        \"xQNTkIrt5bvAXhVmKvBxM\",\n        \"hTkLQGX8fKKkms2V_vm0r\",\n        \"mnKhSKUhk7EFCekLtz1fQ\"\n      ]\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 809,\n      \"versionNonce\": 1080579118,\n      \"isDeleted\": false,\n      \"id\": \"-CvZWfngqmHMBLZxpCqri\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 732.0145089285714,\n      \"y\": 269.67131696428584,\n      \"strokeColor\": \"#aeea00\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 119,\n      \"height\": 35,\n      \"seed\": 415462574,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 28,\n      \"fontFamily\": 1,\n      \"text\": \"Frontend\",\n      \"baseline\": 25,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"rectangle\",\n      \"version\": 1500,\n      \"versionNonce\": 404278706,\n      \"isDeleted\": false,\n      \"id\": \"MSfRFHL5NGAG-wZc7hZub\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 664.8816964285714,\n      \"y\": 117.36356026785717,\n      \"strokeColor\": \"#aeea00\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 252.3643973214285,\n      \"height\": 231.47656249999994,\n      \"seed\": 153297006,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [\n        \"CzbKjuh5LlBF8vEsdGoYs\",\n        \"euFzlspcyBdei7ZjDGsTE\"\n      ]\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 346,\n      \"versionNonce\": 1998441774,\n      \"isDeleted\": false,\n      \"id\": \"HblX02yOmNDLcQXmIx14_\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 318.7762276785714,\n      \"y\": -124.638671875,\n      \"strokeColor\": \"#eceff1\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 244,\n      \"height\": 45,\n      \"seed\": 1398208946,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 36,\n      \"fontFamily\": 1,\n      \"text\": \"Typical Stack\",\n      \"baseline\": 32,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"text\",\n      \"version\": 665,\n      \"versionNonce\": 848204036,\n      \"isDeleted\": false,\n      \"id\": \"oi9Jo0SrqAcTWeuWmDnFT\",\n      \"fillStyle\": \"hachure\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 670.4157366071428,\n      \"y\": 42.34793526785717,\n      \"strokeColor\": \"#eceff1\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 241,\n      \"height\": 45,\n      \"seed\": 1175668722,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": [],\n      \"fontSize\": 36,\n      \"fontFamily\": 1,\n      \"text\": \"Layr Stack\",\n      \"baseline\": 32,\n      \"textAlign\": \"center\",\n      \"verticalAlign\": \"top\"\n    },\n    {\n      \"type\": \"rectangle\",\n      \"version\": 863,\n      \"versionNonce\": 1082652526,\n      \"isDeleted\": false,\n      \"id\": \"snrOZbuQdOfsbwfDVsvOh\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 259.15066964285734,\n      \"y\": -163.32142857142867,\n      \"strokeColor\": \"#eceff1\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 711.9587053571425,\n      \"height\": 743.0351562500001,\n      \"seed\": 2001414322,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"sharp\",\n      \"boundElementIds\": []\n    },\n    {\n      \"type\": \"arrow\",\n      \"version\": 745,\n      \"versionNonce\": 1705508466,\n      \"isDeleted\": false,\n      \"id\": \"mnKhSKUhk7EFCekLtz1fQ\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 1,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 795.6113691236103,\n      \"y\": 251.49637276785717,\n      \"strokeColor\": \"#aeea00\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 0.5444303450863117,\n      \"height\": 29.553292410714278,\n      \"seed\": 1730055982,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"round\",\n      \"boundElementIds\": [],\n      \"startBinding\": {\n        \"elementId\": \"pCkj2Qaeh9PDMAzSCWPM9\",\n        \"focus\": 0.032282012887604794,\n        \"gap\": 1\n      },\n      \"endBinding\": {\n        \"elementId\": \"BK5gVfRenINsXjknIgSxf\",\n        \"focus\": -0.040579986296433085,\n        \"gap\": 1.3568638392857224\n      },\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          0.5444303450863117,\n          -29.553292410714278\n        ]\n      ],\n      \"lastCommittedPoint\": null\n    },\n    {\n      \"type\": \"arrow\",\n      \"version\": 54,\n      \"versionNonce\": 79913010,\n      \"isDeleted\": false,\n      \"id\": \"euFzlspcyBdei7ZjDGsTE\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"angle\": 0,\n      \"x\": 583.1283482142857,\n      \"y\": 234.59709821428572,\n      \"strokeColor\": \"#eceff1\",\n      \"backgroundColor\": \"transparent\",\n      \"width\": 62.05624021153369,\n      \"height\": 0.7102786712734144,\n      \"seed\": 1376797106,\n      \"groupIds\": [],\n      \"strokeSharpness\": \"round\",\n      \"boundElementIds\": [],\n      \"startBinding\": {\n        \"elementId\": \"zk1Tzfs7TN7NuvV9nUrkK\",\n        \"focus\": 1.4825585590033774,\n        \"gap\": 16.743582589285722\n      },\n      \"endBinding\": {\n        \"elementId\": \"MSfRFHL5NGAG-wZc7hZub\",\n        \"focus\": 0.0075499005148347426,\n        \"gap\": 19.697108002752145\n      },\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          62.05624021153369,\n          -0.7102786712734144\n        ]\n      ],\n      \"lastCommittedPoint\": null\n    }\n  ],\n  \"appState\": {\n    \"viewBackgroundColor\": \"#000\",\n    \"gridSize\": null\n  }\n}\n"
  },
  {
    "path": "docs/build.js",
    "content": "const {buildDocumentation, freezeDocumentation} = require('@mvila/simple-doc');\nconst {copySync, removeSync, readJsonSync, writeJSONSync} = require('fs-extra');\nconst path = require('path');\n\nconst VERSION = 'v2';\n\nconst SOURCE_DIRECTORY = './contents';\nconst BUILD_DIRECTORY = './build';\nconst WEBSITE_DIRECTORY = '../website/frontend/public/docs';\nconst WEBSITE_INDEX_FILE = '../website/frontend/src/docs.json';\n\nconst sourceDirectory = path.resolve(__dirname, SOURCE_DIRECTORY);\nconst buildDirectory = path.resolve(__dirname, BUILD_DIRECTORY);\nconst websiteDirectory = path.resolve(__dirname, WEBSITE_DIRECTORY);\nconst websiteIndexFile = path.resolve(__dirname, WEBSITE_INDEX_FILE);\n\nbuildDocumentation(sourceDirectory, buildDirectory);\n\nfreezeDocumentation(buildDirectory);\n\nconsole.log(`Writing index file to '${path.relative(process.cwd(), websiteIndexFile)}'...`);\n\nconst builtIndexFile = path.join(buildDirectory, 'index.json');\n\nconst builtIndex = readJsonSync(builtIndexFile);\nconst websiteIndex = readJsonSync(websiteIndexFile);\n\nwebsiteIndex.versions[VERSION] = builtIndex;\n\nwriteJSONSync(websiteIndexFile, websiteIndex, {spaces: 2});\n\nconst websiteDirectoryWithVersion = path.join(websiteDirectory, VERSION);\n\nconsole.log(\n  `Copying contents to '${path.relative(process.cwd(), websiteDirectoryWithVersion)}'...`\n);\n\nremoveSync(websiteDirectoryWithVersion);\n\ncopySync(buildDirectory, websiteDirectoryWithVersion, {\n  filter(source) {\n    return source !== builtIndexFile;\n  }\n});\n"
  },
  {
    "path": "docs/contents/concepts/coming-soon.md",
    "content": "### Concepts\n\n#### Coming Soon\n\nThis section will introduce the basic concepts of Layr, such as:\n\n- Components\n- Controlled attributes and methods\n- Cross-layer inheritance\n- Storage\n- Routes and wrappers\n\nUnfortunately, writing documentation takes a lot of time, and the Layr project is currently handled by a [single person](https://mvila.me) who has to work for customers to make a living and is also starting a new [ambitious project](https://1place.app), which will be based on Layr.\n\nMore documentation will come for sure, but please be patient.\n\nIf you cannot wait and feel adventurous, you can figure out how to build an app with Layr by checking out:\n\n- The [\"Hello, World!\"](https://layrjs.com/docs/v2/introduction/hello-world) introductory app.\n- The pretty exhaustive [\"Reference\"](https://layrjs.com/docs/v2/reference) section.\n- Some [examples](https://layrjs.com/docs/v2/introduction/introduction#examples) of simple full-stack apps built with Layr.\n"
  },
  {
    "path": "docs/contents/guides/coming-soon.md",
    "content": "### Guides\n\n#### Coming Soon\n\nThis section will contain some guides explaining how to achieve the most common tasks and features with Layr, such as:\n\n- Local development\n- Layouts and pages\n- Data fetching and storage\n- Authentication\n- Testing\n- Deployment\n\nUnfortunately, writing documentation takes a lot of time, and the Layr project is currently handled by a [single person](https://mvila.me) who has to work for customers to make a living and is also starting a new [ambitious project](https://1place.app), which will be based on Layr.\n\nMore documentation will come for sure, but please be patient.\n\nIf you cannot wait and feel adventurous, you can figure out how to build an app with Layr by checking out:\n\n- The [\"Hello, World!\"](https://layrjs.com/docs/v2/introduction/hello-world) introductory app.\n- The pretty exhaustive [\"Reference\"](https://layrjs.com/docs/v2/reference) section.\n- Some [examples](https://layrjs.com/docs/v2/introduction/introduction#examples) of simple full-stack apps built with Layr.\n"
  },
  {
    "path": "docs/contents/index.json",
    "content": "{\n  \"books\": [\n    {\n      \"title\": \"Getting Started\",\n      \"slug\": \"introduction\",\n      \"chapters\": [\n        {\n          \"title\": \"Introduction\",\n          \"slug\": \"introduction\",\n          \"file\": \"introduction/introduction.md\"\n        },\n        {\n          \"title\": \"Hello, World!\",\n          \"slug\": \"hello-world\",\n          \"file\": \"introduction/hello-world/hello-world.md\",\n          \"assets\": \"introduction/hello-world/assets\"\n        },\n        {\n          \"title\": \"Storing Data\",\n          \"slug\": \"storing-data\",\n          \"file\": \"introduction/storing-data.md\"\n        },\n        {\n          \"title\": \"Handling Authorization\",\n          \"slug\": \"handling-authorization\",\n          \"file\": \"introduction/handling-authorization.md\"\n        }\n      ]\n    },\n    {\n      \"title\": \"Concepts\",\n      \"slug\": \"concepts\",\n      \"chapters\": [\n        {\n          \"title\": \"Coming Soon\",\n          \"slug\": \"coming-soon\",\n          \"file\": \"concepts/coming-soon.md\"\n        }\n      ]\n    },\n    {\n      \"title\": \"Guides\",\n      \"slug\": \"guides\",\n      \"chapters\": [\n        {\n          \"title\": \"Coming Soon\",\n          \"slug\": \"coming-soon\",\n          \"file\": \"guides/coming-soon.md\"\n        }\n      ]\n    },\n    {\n      \"title\": \"Reference\",\n      \"slug\": \"reference\",\n      \"chapters\": [\n        {\n          \"title\": \"Component\",\n          \"slug\": \"component\",\n          \"file\": \"reference/component.md\",\n          \"source\": [\n            \"../../packages/component/src/component.ts\",\n            \"../../packages/component/src/cloning.ts\",\n            \"../../packages/component/src/forking.ts\",\n            \"../../packages/component/src/merging.ts\",\n            \"../../packages/component/src/serialization.ts\",\n            \"../../packages/component/src/deserialization.ts\",\n            \"../../packages/component/src/decorators.ts\",\n            \"../../packages/component/src/utilities.ts\"\n          ],\n          \"category\": \"Basics\"\n        },\n        {\n          \"title\": \"EmbeddedComponent\",\n          \"slug\": \"embedded-component\",\n          \"file\": \"reference/embedded-component.md\",\n          \"source\": \"../../packages/component/src/embedded-component.ts\",\n          \"category\": \"Basics\"\n        },\n        {\n          \"title\": \"Property\",\n          \"slug\": \"property\",\n          \"file\": \"reference/property.md\",\n          \"source\": \"../../packages/component/src/properties/property.ts\",\n          \"category\": \"Basics\"\n        },\n        {\n          \"title\": \"Attribute\",\n          \"slug\": \"attribute\",\n          \"file\": \"reference/attribute.md\",\n          \"source\": \"../../packages/component/src/properties/attribute.ts\",\n          \"category\": \"Basics\"\n        },\n        {\n          \"title\": \"IdentifierAttribute\",\n          \"slug\": \"identifier-attribute\",\n          \"file\": \"reference/identifier-attribute.md\",\n          \"source\": \"../../packages/component/src/properties/identifier-attribute.ts\",\n          \"category\": \"Basics\"\n        },\n        {\n          \"title\": \"PrimaryIdentifierAttribute\",\n          \"slug\": \"primary-identifier-attribute\",\n          \"file\": \"reference/primary-identifier-attribute.md\",\n          \"source\": \"../../packages/component/src/properties/primary-identifier-attribute.ts\",\n          \"category\": \"Basics\"\n        },\n        {\n          \"title\": \"SecondaryIdentifierAttribute\",\n          \"slug\": \"secondary-identifier-attribute\",\n          \"file\": \"reference/secondary-identifier-attribute.md\",\n          \"source\": \"../../packages/component/src/properties/secondary-identifier-attribute.ts\",\n          \"category\": \"Basics\"\n        },\n        {\n          \"title\": \"Method\",\n          \"slug\": \"method\",\n          \"file\": \"reference/method.md\",\n          \"source\": \"../../packages/component/src/properties/method.ts\",\n          \"category\": \"Basics\"\n        },\n        {\n          \"title\": \"ValueType\",\n          \"slug\": \"value-type\",\n          \"file\": \"reference/value-type.md\",\n          \"source\": \"../../packages/component/src/properties/value-types/value-type.ts\",\n          \"category\": \"Basics\"\n        },\n        {\n          \"title\": \"Sanitizer\",\n          \"slug\": \"sanitizer\",\n          \"file\": \"reference/sanitizer.md\",\n          \"source\": \"../../packages/component/src/sanitization/sanitizer.ts\",\n          \"category\": \"Basics\"\n        },\n        {\n          \"title\": \"Validator\",\n          \"slug\": \"validator\",\n          \"file\": \"reference/validator.md\",\n          \"source\": \"../../packages/component/src/validation/validator.ts\",\n          \"category\": \"Basics\"\n        },\n        {\n          \"title\": \"AttributeSelector\",\n          \"slug\": \"attribute-selector\",\n          \"file\": \"reference/attribute-selector.md\",\n          \"source\": \"../../packages/component/src/properties/attribute-selector.ts\",\n          \"category\": \"Basics\"\n        },\n        {\n          \"title\": \"IdentityMap\",\n          \"slug\": \"identity-map\",\n          \"file\": \"reference/identity-map.md\",\n          \"source\": \"../../packages/component/src/identity-map.ts\",\n          \"category\": \"Basics\"\n        },\n        {\n          \"title\": \"ComponentClient\",\n          \"slug\": \"component-client\",\n          \"file\": \"reference/component-client.md\",\n          \"source\": \"../../packages/component-client/src/component-client.ts\",\n          \"category\": \"Communication\"\n        },\n        {\n          \"title\": \"ComponentServer\",\n          \"slug\": \"component-server\",\n          \"file\": \"reference/component-server.md\",\n          \"source\": \"../../packages/component-server/src/component-server.ts\",\n          \"category\": \"Communication\"\n        },\n        {\n          \"title\": \"ComponentHTTPClient\",\n          \"slug\": \"component-http-client\",\n          \"file\": \"reference/component-http-client.md\",\n          \"source\": \"../../packages/component-http-client/src/component-http-client.ts\",\n          \"category\": \"Communication\"\n        },\n        {\n          \"title\": \"ComponentHTTPServer\",\n          \"slug\": \"component-http-server\",\n          \"file\": \"reference/component-http-server.md\",\n          \"source\": \"../../packages/component-http-server/src/component-http-server.ts\",\n          \"category\": \"Communication\"\n        },\n        {\n          \"title\": \"component-express-middleware\",\n          \"slug\": \"component-express-middleware\",\n          \"file\": \"reference/component-express-middleware.md\",\n          \"source\": \"../../packages/component-express-middleware/src/component-express-middleware.ts\",\n          \"category\": \"Communication\"\n        },\n        {\n          \"title\": \"component-koa-middleware\",\n          \"slug\": \"component-koa-middleware\",\n          \"file\": \"reference/component-koa-middleware.md\",\n          \"source\": \"../../packages/component-koa-middleware/src/component-koa-middleware.ts\",\n          \"category\": \"Communication\"\n        },\n        {\n          \"title\": \"Storable()\",\n          \"slug\": \"storable\",\n          \"file\": \"reference/storable.md\",\n          \"source\": [\n            \"../../packages/storable/src/storable.ts\",\n            \"../../packages/storable/src/decorators.ts\",\n            \"../../packages/storable/src/utilities.ts\"\n          ],\n          \"category\": \"Storage\"\n        },\n        {\n          \"title\": \"StorableProperty\",\n          \"slug\": \"storable-property\",\n          \"file\": \"reference/storable-property.md\",\n          \"source\": [\"../../packages/storable/src/properties/storable-property.ts\"],\n          \"category\": \"Storage\"\n        },\n        {\n          \"title\": \"StorableAttribute\",\n          \"slug\": \"storable-attribute\",\n          \"file\": \"reference/storable-attribute.md\",\n          \"source\": [\"../../packages/storable/src/properties/storable-attribute.ts\"],\n          \"category\": \"Storage\"\n        },\n        {\n          \"title\": \"StorablePrimaryIdentifierAttribute\",\n          \"slug\": \"storable-primary-identifier-attribute\",\n          \"file\": \"reference/storable-primary-identifier-attribute.md\",\n          \"source\": [\n            \"../../packages/storable/src/properties/storable-primary-identifier-attribute.ts\"\n          ],\n          \"category\": \"Storage\"\n        },\n        {\n          \"title\": \"StorableSecondaryIdentifierAttribute\",\n          \"slug\": \"storable-secondary-identifier-attribute\",\n          \"file\": \"reference/storable-secondary-identifier-attribute.md\",\n          \"source\": [\n            \"../../packages/storable/src/properties/storable-secondary-identifier-attribute.ts\"\n          ],\n          \"category\": \"Storage\"\n        },\n        {\n          \"title\": \"StorableMethod\",\n          \"slug\": \"storable-method\",\n          \"file\": \"reference/storable-method.md\",\n          \"source\": [\"../../packages/storable/src/properties/storable-method.ts\"],\n          \"category\": \"Storage\"\n        },\n        {\n          \"title\": \"Query\",\n          \"slug\": \"query\",\n          \"file\": \"reference/query.md\",\n          \"source\": \"../../packages/storable/src/query.ts\",\n          \"category\": \"Storage\"\n        },\n        {\n          \"title\": \"Index\",\n          \"slug\": \"index\",\n          \"file\": \"reference/index.md\",\n          \"source\": \"../../packages/storable/src/index-class.ts\",\n          \"category\": \"Storage\"\n        },\n        {\n          \"title\": \"Store\",\n          \"slug\": \"store\",\n          \"file\": \"reference/store.md\",\n          \"source\": \"../../packages/store/src/store.ts\",\n          \"category\": \"Storage\"\n        },\n        {\n          \"title\": \"MongoDBStore\",\n          \"slug\": \"mongodb-store\",\n          \"file\": \"reference/mongodb-store.md\",\n          \"source\": \"../../packages/mongodb-store/src/mongodb-store.ts\",\n          \"category\": \"Storage\"\n        },\n        {\n          \"title\": \"MemoryStore\",\n          \"slug\": \"memory-store\",\n          \"file\": \"reference/memory-store.md\",\n          \"source\": \"../../packages/memory-store/src/memory-store.ts\",\n          \"category\": \"Storage\"\n        },\n        {\n          \"title\": \"Routable()\",\n          \"slug\": \"routable\",\n          \"file\": \"reference/routable.md\",\n          \"source\": [\n            \"../../packages/routable/src/routable.ts\",\n            \"../../packages/routable/src/decorators.ts\",\n            \"../../packages/routable/src/utilities.ts\"\n          ],\n          \"category\": \"Routing\"\n        },\n        {\n          \"title\": \"Addressable\",\n          \"slug\": \"addressable\",\n          \"file\": \"reference/addressable.md\",\n          \"source\": \"../../packages/routable/src/addressable.ts\",\n          \"category\": \"Routing\"\n        },\n        {\n          \"title\": \"Route\",\n          \"slug\": \"route\",\n          \"file\": \"reference/route.md\",\n          \"source\": \"../../packages/routable/src/route.ts\",\n          \"category\": \"Routing\"\n        },\n        {\n          \"title\": \"Wrapper\",\n          \"slug\": \"wrapper\",\n          \"file\": \"reference/wrapper.md\",\n          \"source\": \"../../packages/routable/src/wrapper.ts\",\n          \"category\": \"Routing\"\n        },\n        {\n          \"title\": \"Navigator\",\n          \"slug\": \"navigator\",\n          \"file\": \"reference/navigator.md\",\n          \"source\": [\n            \"../../packages/navigator/src/navigator.ts\",\n            \"../../packages/navigator/src/utilities.ts\"\n          ],\n          \"category\": \"Routing\"\n        },\n        {\n          \"title\": \"BrowserNavigator\",\n          \"slug\": \"browser-navigator\",\n          \"file\": \"reference/browser-navigator.md\",\n          \"source\": \"../../packages/browser-navigator/src/browser-navigator.ts\",\n          \"category\": \"Routing\"\n        },\n        {\n          \"title\": \"MemoryNavigator\",\n          \"slug\": \"memory-navigator\",\n          \"file\": \"reference/memory-navigator.md\",\n          \"source\": \"../../packages/memory-navigator/src/memory-navigator.ts\",\n          \"category\": \"Routing\"\n        },\n        {\n          \"title\": \"WithRoles()\",\n          \"slug\": \"with-roles\",\n          \"file\": \"reference/with-roles.md\",\n          \"source\": [\n            \"../../packages/with-roles/src/with-roles.ts\",\n            \"../../packages/with-roles/src/decorators.ts\",\n            \"../../packages/with-roles/src/utilities.ts\"\n          ],\n          \"category\": \"Authorization\"\n        },\n        {\n          \"title\": \"Role\",\n          \"slug\": \"role\",\n          \"file\": \"reference/role.md\",\n          \"source\": \"../../packages/with-roles/src/role.ts\",\n          \"category\": \"Authorization\"\n        },\n        {\n          \"title\": \"aws-integration\",\n          \"slug\": \"aws-integration\",\n          \"file\": \"reference/aws-integration.md\",\n          \"source\": [\n            \"../../packages/aws-integration/src/index.ts\",\n            \"../../packages/aws-integration/src/lambda-handler.ts\"\n          ],\n          \"category\": \"Integrations\"\n        },\n        {\n          \"title\": \"react-integration\",\n          \"slug\": \"react-integration\",\n          \"file\": \"reference/react-integration.md\",\n          \"source\": [\n            \"../../packages/react-integration/src/index.ts\",\n            \"../../packages/react-integration/src/components.tsx\",\n            \"../../packages/react-integration/src/decorators.tsx\",\n            \"../../packages/react-integration/src/hooks.ts\"\n          ],\n          \"category\": \"Integrations\"\n        },\n        {\n          \"title\": \"Observable()\",\n          \"slug\": \"observable\",\n          \"file\": \"reference/observable.md\",\n          \"source\": \"../../packages/observable/src/observable.ts\",\n          \"category\": \"Utilities\"\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "docs/contents/introduction/handling-authorization.md",
    "content": "### Handling Authorization\n\n#### Coming Soon\n\nThis tutorial will expand the [\"Hello, World!\"](https://layrjs.com/docs/v2/introduction/hello-world) app to show you how Layr handles user authorization.\n\nUnfortunately, writing tutorials takes a lot of time, and the Layr project is currently handled by a [single person](https://mvila.me) who has to work for customers to make a living and is also starting a new [ambitious project](https://1place.app), which will be based on Layr.\n\nThis tutorial will come for sure, but please be patient.\n\nIf you cannot wait and feel adventurous, you can figure out how to handle authorization with Layr by checking out:\n\n- The [`WithRoles()`](https://layrjs.com/docs/v2/reference/with-roles) mixin in the \"Reference\" section.\n- Some examples of simple full-stack apps handling authorization with Layr:\n  - [Layr Website (TS)](https://github.com/layrjs/layr/tree/master/website)\n  - [CodebaseShow (TS)](https://github.com/codebaseshow/codebaseshow)\n  - [RealWorld Example App (JS)](https://github.com/layrjs/react-layr-realworld-example-app)\n"
  },
  {
    "path": "docs/contents/introduction/hello-world/hello-world.md",
    "content": "### Hello, World!\n\nLet's start our journey into Layr by implementing the mandatory [\"Hello, World!\"](https://en.wikipedia.org/wiki/%22Hello,_World!%22_program) app, and let's make it object-oriented and full-stack!\n\n> **Note**: Layr supports both [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript) and [TypeScript](https://www.typescriptlang.org/). To select your language of choice, use the drop-down menu on the left.\n\n> **TLDR**: The completed app is available in the <!-- <if language=\"js\"> -->[Layr repository](https://github.com/layrjs/layr/tree/master/examples/v2/hello-world-js)<!-- </if> --><!-- <if language=\"ts\"> -->[Layr repository](https://github.com/layrjs/layr/tree/master/examples/v2/hello-world-ts)<!-- </if> -->.\n\n#### Prerequisites\n\n- You use a Mac with a recent version of macOS. Everything should work fine on Linux, but we haven't tested it yet. It may work on Windows, but we haven't tested it and don't plan to provide support for now.\n- You have [Node.js](https://nodejs.org/) v16 or newer installed.\n- You are familiar with [React](https://reactjs.org/), which is used in this tutorial.\n\n#### Creating the App\n\nTo make things easier, we'll use [Boostr](https://boostr.dev) to create the app and manage the local development environment.\n\nBoostr is a companion tool for Layr that takes care of everything you need to build and deploy a Layr app so you can focus on what really matters — the app's code.\n\nSo, first of all, we have to install Boostr. Run the following command in your terminal:\n\n```sh\nnpm install --global boostr\n```\n\n> **Notes**:\n>\n> - Depending on your Node.js installation, you may have to prefix the command with `sudo` so the package can be installed globally.\n> - Installing an NPM package globally is usually not recommended. But it's not a problem in this case because each app managed by Boostr uses a local Boostr package which is automatically installed. So the global Boostr package can be seen as a shortcut to the local Boostr packages installed in your apps, and, therefore, you can have different apps using different versions of Boostr.\n\nThen, run the following commands to create a directory for the app and initialize it:\n\n<!-- <if language=\"js\"> -->\n\n```sh\nmkdir hello-world\ncd hello-world\nboostr initialize @boostr/web-app-js\n```\n\n<!-- </if> -->\n\n<!-- <if language=\"ts\"> -->\n\n```sh\nmkdir hello-world\ncd hello-world\nboostr initialize @boostr/web-app-ts\n```\n\n<!-- </if> -->\n\nFinally, you can open the `hello-world` directory in your favorite [IDE](https://en.wikipedia.org/wiki/Integrated_development_environment) to explore the initial codebase.\n\n> **Note**: You can use any IDE you want, but if you use [Visual Studio Code](https://code.visualstudio.com/), you can profit from the VS Code configuration included in the [Boostr app templates](https://boostr.dev/docs#boostr-initialize-template-options). Otherwise, you may have to set up your IDE to get a suitable configuration.\n\nWe will not detail all directories and files created by Boostr because it would be out of the scope of this tutorial. If you are the kind of person who needs to understand everything, please check out the [Boostr documentation](https://boostr.dev/docs) to find out more.\n\nSo, we will only focus on two files:\n\n<!-- <if language=\"js\"> -->\n\n- `frontend/src/components/application.jsx`: The root [component](https://layrjs.com/docs/v2/reference/component) of the app frontend.\n- `backend/src/components/application.js`: The root [component](https://layrjs.com/docs/v2/reference/component) of the app backend.\n\n<!-- </if> -->\n\n<!-- <if language=\"ts\"> -->\n\n- `frontend/src/components/application.tsx`: The root [component](https://layrjs.com/docs/v2/reference/component) of the app frontend.\n- `backend/src/components/application.ts`: The root [component](https://layrjs.com/docs/v2/reference/component) of the app backend.\n\n<!-- </if> -->\n\n#### Starting the App\n\nStart the app in development mode with the following command:\n\n```sh\nboostr start\n```\n\nThe terminal should output something like this:\n\n```txt\n[database] MongoDB server started at mongodb://localhost:18160/\n[backend] Build succeeded (bundle size: 2.06MB)\n[backend] Component HTTP server started at http://localhost:18159/\n[frontend] Build succeeded (bundle size: 1.34MB)\n[frontend] Single-page application server started at http://localhost:18158/\n```\n\n> **Notes**:\n>\n> - The TCP ports used for each [local development URL](https://boostr.dev/docs#local-development-urls) were randomly set when the Boostr `initialize` command was executed to create the app in the [previous section](https://layrjs.com/docs/v2/introduction/hello-world#creating-the-app). So, it's normal if the TCP ports are different for you.\n> - Don't be freaked out by the size of the generated bundles in development mode. When you deploy your apps, the generated bundles are a lot smaller.\n\nThe last line in the terminal output should include an URL you can open in a browser to display the app.\n\nAt this point, you should see something like this in your browser:\n\n<p>\n\t<img src=\"https://layrjs.com/docs/v2/introduction/hello-world/assets/screenshot-001.immutable.png\" alt=\"Screenshot of the app initialized by Boostr\" style=\"width: 100%; margin-top: .5rem\">\n</p>\n\n#### Taking a Look at the Initial App\n\nLet's see what we have for now.\n\nWhen we bootstrapped the app with the Boostr `initialize` command, we got a minimal app which does one thing: displaying the result of a backend method in the frontend.\n\n##### Frontend\n\nHere's what the initial frontend root [component](https://layrjs.com/docs/v2/reference/component) (`Application`) looks like:\n\n```js\n// JS\n\n// frontend/src/components/application.jsx\n\nimport {Routable} from '@layr/routable';\nimport React from 'react';\nimport {layout, page, useData} from '@layr/react-integration';\n\nexport const extendApplication = (Base) => {\n  class Application extends Routable(Base) {\n    @layout('/') static MainLayout({children}) {\n      return (\n        <>\n          <this.MainPage.Link>\n            <h1>{process.env.APPLICATION_NAME}</h1>\n          </this.MainPage.Link>\n          {children()}\n        </>\n      );\n    }\n\n    @page('[/]') static MainPage() {\n      return useData(\n        async () => await this.isHealthy(),\n\n        (isHealthy) => <p>The app is {isHealthy ? 'healthy' : 'unhealthy'}.</p>\n      );\n    }\n\n    @page('[/]*') static NotFoundPage() {\n      return (\n        <>\n          <h2>Page not found</h2>\n          <p>Sorry, there is nothing here.</p>\n        </>\n      );\n    }\n  }\n\n  return Application;\n};\n```\n\n```ts\n// TS\n\n// frontend/src/components/application.tsx\n\nimport {Routable} from '@layr/routable';\nimport React, {Fragment} from 'react';\nimport {layout, page, useData} from '@layr/react-integration';\n\nimport type {Application as BackendApplication} from '../../../backend/src/components/application';\n\nexport const extendApplication = (Base: typeof BackendApplication) => {\n  class Application extends Routable(Base) {\n    declare ['constructor']: typeof Application;\n\n    @layout('/') static MainLayout({children}: {children: () => any}) {\n      return (\n        <>\n          <this.MainPage.Link>\n            <h1>{process.env.APPLICATION_NAME}</h1>\n          </this.MainPage.Link>\n          {children()}\n        </>\n      );\n    }\n\n    @page('[/]') static MainPage() {\n      return useData(\n        async () => await this.isHealthy(),\n\n        (isHealthy) => <p>The app is {isHealthy ? 'healthy' : 'unhealthy'}.</p>\n      );\n    }\n\n    @page('[/]*') static NotFoundPage() {\n      return (\n        <>\n          <h2>Page not found</h2>\n          <p>Sorry, there is nothing here.</p>\n        </>\n      );\n    }\n  }\n\n  return Application;\n};\n\nexport declare const Application: ReturnType<typeof extendApplication>;\n\nexport type Application = InstanceType<typeof Application>;\n```\n\nIf we put aside the [mixin mechanism](https://www.typescriptlang.org/docs/handbook/mixins.html) that allows the frontend `Application` component to \"inherit\" from the backend `Application` component, we can see three class methods.\n\n`MainLayout()` implements a layout for all the pages of the app:\n\n- It is decorated with [`@layout('/')`](https://layrjs.com/docs/v2/reference/react-integration#layout-decorator), which makes the method acts as a layout and defines an URL path (`'/'`) for it.\n- It renders the name of the app (using the `APPLICATION_NAME` [environment variable](https://github.com/boostrjs/boostr#environment-variables)) in an `<h1>` HTML tag, which is nested into a link pointing to the app's main page (using the [`<this.MainPage.Link>`](https://layrjs.com/docs/v2/reference/routable#route-decorator) React element).\n- It calls the `children` prop to render the content of the pages using this layout.\n\n`MainPage()` implements the main page of the app:\n\n- It is decorated with [`@page('[/]')`](https://layrjs.com/docs/v2/reference/react-integration#page-decorator), which makes the method acts as a page associated with the `'/'` URL path and specifies that the main layout (`'/'` enclosed in square brackets `'[]'`) should be used.\n- It returns the result of the [`useData()`](https://layrjs.com/docs/v2/reference/react-integration#use-data-react-hook) React hook, which calls a backend method (with `await this.isHealthy()`) and renders its result (with `<p>The app is {isHealthy ? 'healthy' : 'unhealthy'}.</p>`).\n\n`NotFoundPage()` implements a page that is displayed when a user goes to an URL that is not handled by the app:\n\n- It is decorated with [`@page('[/]*')`](https://layrjs.com/docs/v2/reference/react-integration#page-decorator), which makes the method acts as a page associated with any unhandled URL path starting with `'/'` and specifies that the main layout (`'/'` enclosed in square brackets `'[]'`) should be used.\n- It renders some simple HTML tags and texts indicating that the page cannot be found.\n\n##### Backend\n\nHere's what the initial backend root [component](https://layrjs.com/docs/v2/reference/component) (`Application`) looks like:\n\n```js\n// JS\n\n// backend/src/components/application.js\n\nimport {Component, method, expose} from '@layr/component';\n\nexport class Application extends Component {\n  @expose({call: true}) @method() static async isHealthy() {\n    return true;\n  }\n}\n```\n\n```ts\n// TS\n\n// backend/src/components/application.ts\n\nimport {Component, method, expose} from '@layr/component';\n\nexport class Application extends Component {\n  @expose({call: true}) @method() static async isHealthy() {\n    return true;\n  }\n}\n```\n\nThis component is straightforward.\n\n`isHealthy()` implements a class method that the frontend can call to check whether the app is healthy:\n\n- It is decorated with [`@expose({call: true})`](https://layrjs.com/docs/v2/reference/component#expose-decorator), which exposes the method to the frontend.\n- It is also decorated with [`@method()`](https://layrjs.com/docs/v2/reference/component#method-decorator), which is required so that the `@expose()` decorator can be used.\n- It always returns `true`, which is rather pointless. An actual `isHealthy()` method would, for example, check whether a database responds correctly.\n\n#### Displaying \"Hello, World!\" in the Frontend\n\nIt's about time to write some code.\n\nWe'll start gently by modifying the frontend to display \"Hello, World!\" instead of \"The app is healthy.\".\n\nModify the `MainPage()` class method in the <!-- <if language=\"js\"> -->`frontend/src/components/application.jsx`<!-- </if> --><!-- <if language=\"ts\"> -->`frontend/src/components/application.tsx`<!-- </if> --> file as follows:\n\n```ts\n@page('[/]') static MainPage() {\n  return <p>Hello, World!</p>;\n}\n```\n\nSave the file, and your browser should automatically refresh the page with the following contents:\n\n<p>\n\t<img src=\"https://layrjs.com/docs/v2/introduction/hello-world/assets/screenshot-002.immutable.png\" alt=\"Screenshot of the app displaying 'Hello, World!'\" style=\"width: 100%; margin-top: .5rem\">\n</p>\n\n#### Adding a Page in the Frontend\n\nSo the frontend is now displaying \"Hello, World!\" and we could call it a day.\n\nBut that was a bit too easy, don't you think?\n\nLet's spice this tutorial a little by adding a page in charge of displaying the \"Hello, World!\" message.\n\nAdd the following class method in the <!-- <if language=\"js\"> -->`frontend/src/components/application.jsx`<!-- </if> --><!-- <if language=\"ts\"> -->`frontend/src/components/application.tsx`<!-- </if> --> file:\n\n```ts\n@page('[/]hello-world') static HelloWorldPage() {\n  return <p>Hello, World!</p>;\n}\n```\n\nThat's it. The app just got a new page. You could view it by changing the URL path to `/hello-world` in your browser, but adding a link to the new page inside the main page would be better in terms of user experience.\n\nLet's do so by modifying the `MainPage()` class method as follows:\n\n```ts\n@page('[/]') static MainPage() {\n  return (\n    <p>\n      <this.HelloWorldPage.Link>\n        See the \"Hello, World!\" page\n      </this.HelloWorldPage.Link>\n    </p>\n  );\n}\n```\n\nHold on. What's going on here? Is it how we create links with Layr? Where is the URL path (`'/hello-world'`) of the \"Hello, World!\" page? Well, in a Layr app, except in the `@page()` decorators, you should never encounter any URL path.\n\nWe hope it doesn't sound too magical because it is not. Any method decorated with [`@page()`](https://layrjs.com/docs/v2/reference/react-integration#page-decorator) automatically gets some attached [shortcut functions](https://layrjs.com/docs/v2/reference/routable#route-decorator), such as `Link()`, which implements a React component rendering a `<a>` tag referencing the URL path of the page.\n\nYour browser should now display the following page:\n\n<p>\n\t<img src=\"https://layrjs.com/docs/v2/introduction/hello-world/assets/screenshot-003.immutable.png\" alt=\"Screenshot of the app displaying a link to the 'Hello, World!' page\" style=\"width: 100%; margin-top: .5rem\">\n</p>\n\n#### Getting the \"Hello, World\" Message from the Backend\n\nAt the beginning of this tutorial, we promised to create a full-stack app, but currently, the backend is not involved, and everything happens in the frontend.\n\nLet's fix that by moving the \"business logic\" generating the \"Hello, World!\" message to the backend.\n\nFirst, add the following class method in the <!-- <if language=\"js\"> -->`backend/src/components/application.js`<!-- </if> --><!-- <if language=\"ts\"> -->`backend/src/components/application.ts`<!-- </if> --> file:\n\n```ts\n@expose({call: true}) @method() static async getHelloWorld() {\n  return 'Hello, World!';\n}\n```\n\nThis method will be callable from the frontend (thanks to the [`@expose()`](https://layrjs.com/docs/v2/reference/component#expose-decorator) and [`@method()`](https://layrjs.com/docs/v2/reference/component#method-decorator) decorators) and will return the `'Hello, World!'` string.\n\n> **Note**: Since the `isHealthy()` class method is not used anymore, you can remove it if you want.\n\nSave the file to restart the backend, and then modify the `HelloWorldPage()` class method in the <!-- <if language=\"js\"> -->`frontend/src/components/application.jsx`<!-- </if> --><!-- <if language=\"ts\"> -->`frontend/src/components/application.tsx`<!-- </if> --> file as follows:\n\n```ts\n@page('[/]hello-world') static HelloWorldPage() {\n  return useData(\n    async () => await this.getHelloWorld(),\n\n    (helloWorld) => <p>{helloWorld}</p>\n  );\n}\n```\n\nAs seen in the initial [`MainPage()`](https://layrjs.com/docs/v2/introduction/hello-world#frontend) method, we use the [`useData()`](https://layrjs.com/docs/v2/reference/react-integration#use-data-react-hook) React hook to call a backend method (with `await this.getHelloWorld()`) and render its result (with `<p>{helloWorld}</p>`).\n\nIf you refresh the \"Hello, World!\" page in your browser, you should not see any difference. However, if you inspect the network requests via the browser's developer tools, you should see that the `'Hello, World'` string displayed in the frontend comes from the backend.\n\n#### One Last Thing\n\nTechnically, we now have a full-stack app involving a frontend and a backend.\n\nBut the app is a bit static. The backend always returns the same `'Hello, World'` string, which is rather boring.\n\nLet's make the app more dynamic by modifying the `getHelloWorld()` class method in the <!-- <if language=\"js\"> -->`backend/src/components/application.js`<!-- </if> --><!-- <if language=\"ts\"> -->`backend/src/components/application.ts`<!-- </if> --> file as follows:\n\n```ts\n@expose({call: true}) @method() static async getHelloWorld() {\n  const translations = ['Hello, World!', 'Bonjour le monde !', 'こんにちは世界！'];\n\n  const translation = translations[Math.round(Math.random() * (translations.length - 1))];\n\n  return translation;\n}\n```\n\nSave the file to restart the backend, and now, when you refresh the \"Hello, World!\" page several times in your browser, you should see some translations randomly picked up.\n\nHere's an example showing the Japanese translation:\n\n<p>\n\t<img src=\"https://layrjs.com/docs/v2/introduction/hello-world/assets/screenshot-004.immutable.png\" alt=\"Screenshot of the app displaying 'Hello, World!' in Japanese\" style=\"width: 100%; margin-top: .5rem\">\n</p>\n\nWe hardcoded the translations in the `getHelloWorld()` class method, which is OK for this tutorial. But in a real-world app, storing the translations in a database would be better so an admin can edit them. We'll see how to achieve that in [another tutorial](https://layrjs.com/docs/v2/introduction/storing-data) that remains to be written.\n\n#### Wrapping Up\n\nIn this first tutorial, we saw how to build a basic full-stack app with Layr:\n\n- We [created an app](https://layrjs.com/docs/v2/introduction/hello-world#creating-the-app) and [started it](https://layrjs.com/docs/v2/introduction/hello-world#starting-the-app) in development mode with a few [Boostr](https://boostr.dev) commands.\n- We discovered how to implement [layouts](https://layrjs.com/docs/v2/introduction/hello-world#taking-a-look-at-the-initial-app), [pages](https://layrjs.com/docs/v2/introduction/hello-world#adding-a-page-in-the-frontend), and [links](https://layrjs.com/docs/v2/introduction/hello-world#adding-a-page-in-the-frontend) encapsulated in a Layr [component](https://layrjs.com/docs/v2/reference/component).\n- We found out how [a backend method could be called from the frontend](https://layrjs.com/docs/v2/introduction/hello-world#getting-the-hello-world-message-from-the-backend) thanks to the cross-layer class inheritance ability of Layr.\n"
  },
  {
    "path": "docs/contents/introduction/introduction.md",
    "content": "### Introduction\n\n> **Note**: Layr v2 is published on NPM, but the documentation is still a work in progress.\n\n#### Overview\n\nLayr is a set of JavaScript/TypeScript libraries that dramatically simplify the development of highly dynamic full-stack apps.\n\nTypically, a highly dynamic full-stack app comprises a frontend and a backend running in two different environments connected through a web API (REST, GraphQL, etc.)\n\nSeparating the frontend and the backend is a good thing. Still, the problem is that building a web API usually leads to a lot of code scattering, knowledge duplication, boilerplate, and accidental complexity.\n\nLayr removes the need to build a web API and [reunites the frontend and backend](https://dev.to/mvila/good-bye-web-apis-2bel) so that you can experience them as a single entity.\n\nOn the frontend side, Layr gives you [routing capabilities](https://layrjs.com/docs/v2/reference/routable) and [object observability](https://layrjs.com/docs/v2/reference/observable) so that you don't need to add an external router or a state manager.\n\nOn the backend side, Layr offers an [ORM](https://layrjs.com/docs/v2/reference/storable) (which can be exposed to the frontend) to make data storage as easy as possible.\n\n#### Made for Highly Dynamic Apps\n\nLayr stands out for building apps that offer rich user interfaces, such as the good old desktop apps.\n\nEven if they both run in a browser, we should clearly differentiate \"websites\" and \"web apps\".\n\n##### Websites\n\nWebsites provide fast load time, good [SEO](https://en.wikipedia.org/wiki/Search_engine_optimization), and a few dynamic parts.\n\nSome examples fitting in this category are e-commerce websites (e.g., [Amazon](https://www.amazon.com/)), marketplace platforms (e.g., [Airbnb](https://www.airbnb.com/)), or online newspapers (e.g., [The New York Times](https://www.nytimes.com/)).\n\nLayr is inappropriate for building these kinds of websites because it relies on a [Single-page application](https://en.wikipedia.org/wiki/Single-page_application) architecture and doesn't provide server-side rendering.\n\nSo, instead of Layr, you should use some frameworks such as [Next.js](https://nextjs.org/), [Nuxt.js](https://nextjs.org/), or [Remix](https://remix.run/).\n\n##### Web Apps\n\nWeb apps provide rich user interfaces and are all made of dynamic parts.\n\nSome examples fitting in this category are productivity apps (e.g., [Notion](https://www.notion.so/)), communication apps (e.g., [Slack](https://slack.com/)), or design apps (e.g., [Figma](https://www.figma.com/)).\n\nLayr is made for building these kinds of apps with a straightforward architecture:\n\n- The frontend exposes an interface for _humans_.\n- The backend exposes an interface for _computers_.\n\nNote that the frontend can obviously run in a browser, but it can also run on mobile and desktop with the help of frameworks such as [React Native](https://reactnative.dev/) or [Electron](https://www.electronjs.org/).\n\n#### Core Features\n\nLayr provides everything you need to build an app from start to finish:\n\n- **Cross-layer inheritance**: A frontend class can \"inherit\" from a backend class, so some exposed [attributes](https://layrjs.com/docs/v2/reference/attribute) are automatically transported between the frontend and the backend. Also, you can call some exposed backend [methods](https://layrjs.com/docs/v2/reference/method) directly from the frontend.\n- **Controlled attributes**: An attribute can be [type-checked](https://layrjs.com/docs/v2/reference/value-type), [sanitized](https://layrjs.com/docs/v2/reference/sanitizer), [validated](https://layrjs.com/docs/v2/reference/validator), [serialized](https://layrjs.com/docs/v2/reference/component#serialization), and [observed](https://layrjs.com/docs/v2/reference/observable) at runtime.\n- **Storage**: A class instance can be [persisted](https://layrjs.com/docs/v2/reference/storable) in a database. Currently, only [MongoDB](https://www.mongodb.com/) is supported, but we plan to support more databases in the future.\n- **Routing**: A method can be [associated with an URL](https://layrjs.com/docs/v2/reference/routable) and controlled by a [navigator](https://layrjs.com/docs/v2/reference/navigator) so that this method is automatically called when the user navigates.\n- **Layouts**: A method can act as a [wrapper](https://layrjs.com/docs/v2/reference/wrapper) for other methods with a shared URL path prefix. This way, you can easily create [layouts](https://layrjs.com/docs/v2/reference/react-integration#layout-decorator) for your [pages](https://layrjs.com/docs/v2/reference/react-integration#page-decorator).\n- **Authorization**: [User-role-based](https://layrjs.com/docs/v2/reference/with-roles) authorizations can be set to restrict some attributes or methods.\n- **Integrations**: Integration helpers are provided to facilitate the integration of the most popular libraries or services. Currently, two integration helpers are available: [react-integration](https://layrjs.com/docs/v2/reference/react-integration) and [aws-integration](https://layrjs.com/docs/v2/reference/aws-integration). But more should come shortly.\n- **Interoperability**: The backend is automatically exposed through a [Deepr API](https://deepr.io), so you can consume it from any frontend even though it's not built with Layr. And if you want to bring a more standard API (e.g., REST) to your backend, it's straightforward to add such an API in your Layr backend.\n\n#### Core Principles\n\nHere's a quick taste of the core principles upon which Layr is built:\n\n- **Object-oriented**: Layr embraces the object-oriented paradigm in all aspects of an app and allows you to organize your code in a way that is as cohesive as possible.\n- **End-to-end type safety**: When you use TypeScript, from the frontend to the database (which goes through the backend), every single piece of a Layr app can be type-safe.\n- **100% DRY**: A Layr app can be fully [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) (i.e., zero knowledge duplication).\n- **Low-level**: Layr is designed to be as closest to the language as possible and can be seen as a [language extension](https://layrjs.com/blog/articles/Getting-the-Right-Level-of-Generalization-7xpk37) in many ways.\n- **Unopinionated**: Layr has strong opinions about itself but doesn't force you to use any external library, service, or tool.\n\n#### Command-Line Interface\n\nLayr is just a set of low-level JavaScript/TypeScript libraries and does not come with a CLI.\n\nSo, you can use Layr with any development and deployment tools.\n\nHowever, we know that setting up a development environment and a deployment mechanism can be challenging.\n\nSo, we created [Boostr](https://boostr.dev), a companion tool that takes care of everything you need to build and deploy a Layr app.\n\nCheck out the [Boostr documentation](https://boostr.dev/docs) to find out more.\n\n#### Compatibility\n\nLayr is implemented in [TypeScript](https://www.typescriptlang.org/), but you can use either JavaScript or TypeScript to build your app.\n\nIf you are using JavaScript, you'll need to compile your code with [Babel](https://babeljs.io/) to take advantage of some novel JavaScript features such as \"decorators\".\n\nIf you are using TypeScript, all you need is the TypeScript compiler ([`tsc`](https://www.typescriptlang.org/docs/handbook/compiler-options.html)).\n\n> **Note**: If you use [Boostr](https://boostr.dev) to manage your app's development and deployment, you don't have to worry about compiling your code because it is automatically handled.\n\nTo run your app, you'll need a JavaScript runtime for both the frontend and the backend.\n\n##### Frontend\n\n###### Web\n\nAny modern browser should work fine.\n\nHere are the minimum versions with which Layr is tested:\n\n- Chrome v55\n- Safari v11\n- Firefox v54\n- Edge Chromium\n\n###### Mobile and Desktop\n\nAny mobile or desktop app framework using JavaScript or TypeScript (such as [React Native](https://reactnative.dev/) or [Electron](https://www.electronjs.org/)) should work fine.\n\n##### Backend\n\nAny environment running [Node.js](https://nodejs.org/) v16 or later is supported.\n\n#### Examples\n\nHere are some examples of simple full-stack apps that you can check out:\n\n- [CRUD Example App (JS)](https://github.com/layrjs/crud-example-app-js-boostr)\n- [CRUD Example App (TS)](https://github.com/layrjs/crud-example-app-ts-boostr)\n- [Layr Website (TS)](https://github.com/layrjs/layr/tree/master/website)\n- [CodebaseShow (TS)](https://github.com/codebaseshow/codebaseshow)\n- [RealWorld Example App (JS)](https://github.com/layrjs/react-layr-realworld-example-app)\n"
  },
  {
    "path": "docs/contents/introduction/storing-data.md",
    "content": "### Storing Data\n\n#### Coming Soon\n\nThis tutorial will expand the [\"Hello, World!\"](https://layrjs.com/docs/v2/introduction/hello-world) app to show you how Layr handles database storage.\n\nUnfortunately, writing tutorials takes a lot of time, and the Layr project is currently handled by a [single person](https://mvila.me) who has to work for customers to make a living and is also starting a new [ambitious project](https://1place.app), which will be based on Layr.\n\nThis tutorial will come for sure, but please be patient.\n\nIf you cannot wait and feel adventurous, you can figure out how to store data with Layr by checking out:\n\n- The [`Storable()`](https://layrjs.com/docs/v2/reference/storable) mixin in the \"Reference\" section.\n- Some [examples](https://layrjs.com/docs/v2/introduction/introduction#examples) of simple full-stack apps built with Layr.\n"
  },
  {
    "path": "docs/package.json",
    "content": "{\n  \"name\": \"@layr/docs\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"description\": \"Layr documentation\",\n  \"keywords\": [],\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/layrjs/layr/tree/master/docs\",\n  \"scripts\": {\n    \"build\": \"node ./build.js\"\n  },\n  \"dependencies\": {\n    \"@mvila/simple-doc\": \"^0.1.138\",\n    \"fs-extra\": \"^11.1.0\"\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter/README.md",
    "content": "# Counter\n\nA simple example to introduce the core concepts of Layr.\n\n## Usage\n\nInstall the npm dependencies with:\n\n```sh\nnpm install\n```\n\nRun the example with:\n\n```sh\nnpx babel-node ./src/frontend.js\n```\n"
  },
  {
    "path": "examples/v1/counter/babel.config.json",
    "content": "{\n  \"presets\": [[\"@babel/preset-env\", {\"targets\": {\"node\": \"10\"}}]],\n  \"plugins\": [\n    [\"@babel/plugin-proposal-decorators\", {\"legacy\": true}],\n    [\"@babel/plugin-proposal-class-properties\"]\n  ]\n}\n"
  },
  {
    "path": "examples/v1/counter/jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n  \"target\": \"ES2017\",\n    \"module\": \"CommonJS\",\n    \"checkJs\": false,\n    \"experimentalDecorators\": true\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter/package.json",
    "content": "{\n  \"name\": \"counter\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A simple example to introduce the core concepts of Layr\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@layr/component\": \"^1.0.0\",\n    \"@layr/component-client\": \"^1.0.0\",\n    \"@layr/component-server\": \"^1.0.0\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.11.1\",\n    \"@babel/node\": \"^7.10.5\",\n    \"@babel/plugin-proposal-class-properties\": \"^7.10.4\",\n    \"@babel/plugin-proposal-decorators\": \"^7.10.5\",\n    \"@babel/preset-env\": \"^7.11.0\"\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter/src/backend.js",
    "content": "import {Component, primaryIdentifier, attribute, method, expose} from '@layr/component';\nimport {ComponentServer} from '@layr/component-server';\n\nclass Counter extends Component {\n  // We need a primary identifier so a Counter instance\n  // can be transported between the frontend and the backend\n  // while keeping it's identity\n  @expose({get: true, set: true}) @primaryIdentifier() id;\n\n  // The counter's value is exposed to the frontend\n  @expose({get: true, set: true}) @attribute('number') value = 0;\n\n  // And the \"business logic\" is exposed as well\n  @expose({call: true}) @method() increment() {\n    this.value++;\n  }\n}\n\n// We serve the Counter through a ComponentServer\nexport const server = new ComponentServer(Counter);\n"
  },
  {
    "path": "examples/v1/counter/src/frontend.js",
    "content": "import {ComponentClient} from '@layr/component-client';\n\nimport {server} from './backend';\n\n// We create a client that is connected to the backend's server\nconst client = new ComponentClient(server);\n\n// We get the backend's Counter component\nconst BackendCounter = client.getComponent();\n\n// We extends the backend's Counter component so we can override the increment() method\nclass Counter extends BackendCounter {\n  increment() {\n    super.increment(); // The backend's `increment()` method is invoked\n    console.log(this.value); // Some additional code is executed in the frontend\n  }\n}\n\n// Lastly, we consume the Counter\nconst counter = new Counter();\ncounter.increment();\n"
  },
  {
    "path": "examples/v1/counter-with-create-react-app-ts/README.md",
    "content": "# Counter with CreateReactApp (TS)\n\nA simple example to introduce the core concepts of Layr.\n\n## Install\n\nInstall the npm dependencies with:\n\n```sh\nnpm install\n```\n\n## Usage\n\n### Running the app in development mode\n\nExecute the following command:\n\n```sh\nnpm run start\n```\n\nThe app should then be available at http://localhost:3000.\n\n### Debugging\n\n#### Client\n\nAdd the following entry in the local storage of your browser:\n\n```\n| Key   | Value     |\n| ----- | --------- |\n| debug | layr:* |\n```\n\n#### Server\n\nAdd the following environment variables when starting the app:\n\n```sh\nDEBUG=layr:* DEBUG_DEPTH=10\n```\n"
  },
  {
    "path": "examples/v1/counter-with-create-react-app-ts/backend/package.json",
    "content": "{\n  \"name\": \"counter-with-create-react-app-ts-backend\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"start\": \"nodemon --watch ./src --exec ts-node ./src/http-server.ts\"\n  },\n  \"dependencies\": {\n    \"@layr/component\": \"^1.0.0\",\n    \"@layr/component-server\": \"^1.0.0\",\n    \"tslib\": \"^2.0.3\"\n  },\n  \"devDependencies\": {\n    \"@layr/component-http-server\": \"^1.0.0\",\n    \"@types/node\": \"^14.11.8\",\n    \"nodemon\": \"^2.0.5\",\n    \"ts-node\": \"^9.0.0\",\n    \"typescript\": \"^4.0.3\"\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter-with-create-react-app-ts/backend/src/components/counter.ts",
    "content": "import {Component, primaryIdentifier, attribute, method, expose} from '@layr/component';\n\nexport class Counter extends Component {\n  @expose({get: true, set: true}) @primaryIdentifier() id!: string;\n\n  @expose({get: true, set: true}) @attribute('number') value = 0;\n\n  @expose({call: true}) @method() async increment() {\n    this.value++;\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter-with-create-react-app-ts/backend/src/http-server.ts",
    "content": "import {ComponentHTTPServer} from '@layr/component-http-server';\n\nimport {server} from './server';\n\nconst httpServer = new ComponentHTTPServer(server, {port: 3001});\nhttpServer.start();\n"
  },
  {
    "path": "examples/v1/counter-with-create-react-app-ts/backend/src/server.ts",
    "content": "import {ComponentServer} from '@layr/component-server';\n\nimport {Counter} from './components/counter';\n\nexport const server = new ComponentServer(Counter);\n"
  },
  {
    "path": "examples/v1/counter-with-create-react-app-ts/backend/tsconfig.json",
    "content": "{\n  \"include\": [\"src/**/*\"],\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"module\": \"CommonJS\",\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"jsx\": \"react\",\n    \"sourceMap\": true,\n    \"importHelpers\": true,\n    \"strict\": true,\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"experimentalDecorators\": true,\n    \"resolveJsonModule\": true,\n    \"stripInternal\": true,\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter-with-create-react-app-ts/frontend/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# production\n/build\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "examples/v1/counter-with-create-react-app-ts/frontend/package.json",
    "content": "{\n  \"name\": \"counter-with-create-react-app-ts-frontend\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test\",\n    \"eject\": \"react-scripts eject\"\n  },\n  \"dependencies\": {\n    \"@layr/component\": \"^1.0.6\",\n    \"@layr/component-http-client\": \"^1.0.3\",\n    \"@layr/react-integration\": \"^1.0.3\",\n    \"@types/node\": \"^14.11.8\",\n    \"@types/react\": \"^16.9.52\",\n    \"@types/react-dom\": \"^16.9.8\",\n    \"react\": \"^16.13.1\",\n    \"react-dom\": \"^16.13.1\",\n    \"react-scripts\": \"3.4.3\",\n    \"typescript\": \"^4.0.3\"\n  },\n  \"eslintConfig\": {\n    \"extends\": \"react-app\"\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.2%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ]\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter-with-create-react-app-ts/frontend/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"icon\" href=\"%PUBLIC_URL%/favicon.ico\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta name=\"theme-color\" content=\"#000000\" />\n    <meta\n      name=\"description\"\n      content=\"Web site created using create-react-app\"\n    />\n    <link rel=\"apple-touch-icon\" href=\"%PUBLIC_URL%/logo192.png\" />\n    <!--\n      manifest.json provides metadata used when your web app is installed on a\n      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/\n    -->\n    <link rel=\"manifest\" href=\"%PUBLIC_URL%/manifest.json\" />\n    <!--\n      Notice the use of %PUBLIC_URL% in the tags above.\n      It will be replaced with the URL of the `public` folder during the build.\n      Only files inside the `public` folder can be referenced from the HTML.\n\n      Unlike \"/favicon.ico\" or \"favicon.ico\", \"%PUBLIC_URL%/favicon.ico\" will\n      work correctly both with client-side routing and a non-root public URL.\n      Learn how to configure a non-root public URL by running `npm run build`.\n    -->\n    <title>React App</title>\n  </head>\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n    <!--\n      This HTML file is a template.\n      If you open it directly in the browser, you will see an empty page.\n\n      You can add webfonts, meta tags, or analytics to this file.\n      The build step will place the bundled scripts into the <body> tag.\n\n      To begin the development, run `npm start` or `yarn start`.\n      To create a production bundle, use `npm run build` or `yarn build`.\n    -->\n  </body>\n</html>\n"
  },
  {
    "path": "examples/v1/counter-with-create-react-app-ts/frontend/public/manifest.json",
    "content": "{\n  \"short_name\": \"React App\",\n  \"name\": \"Create React App Sample\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    },\n    {\n      \"src\": \"logo192.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"192x192\"\n    },\n    {\n      \"src\": \"logo512.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"512x512\"\n    }\n  ],\n  \"start_url\": \".\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "examples/v1/counter-with-create-react-app-ts/frontend/public/robots.txt",
    "content": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "examples/v1/counter-with-create-react-app-ts/frontend/src/components/counter.tsx",
    "content": "import {ComponentHTTPClient} from '@layr/component-http-client';\nimport React from 'react';\nimport {view} from '@layr/react-integration';\n\nimport type {Counter as BackendCounter} from '../../../backend/src/components/counter';\n\nexport const getCounter = async () => {\n  const client = new ComponentHTTPClient('http://localhost:3001');\n\n  const BackendCounterProxy = (await client.getComponent()) as typeof BackendCounter;\n\n  class Counter extends BackendCounterProxy {\n    @view() Main() {\n      return (\n        <div>\n          <input value={this.value} readOnly />\n          <button\n            onClick={async () => {\n              await this.increment();\n            }}\n          >\n            +\n          </button>\n        </div>\n      );\n    }\n  }\n\n  return Counter;\n};\n"
  },
  {
    "path": "examples/v1/counter-with-create-react-app-ts/frontend/src/index.tsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\n\nimport {getCounter} from './components/counter';\n\n(async () => {\n  const Counter = await getCounter();\n\n  const counter = new Counter();\n\n  ReactDOM.render(\n    <React.StrictMode>\n      <counter.Main />\n    </React.StrictMode>,\n    document.getElementById('root')\n  );\n})();\n"
  },
  {
    "path": "examples/v1/counter-with-create-react-app-ts/frontend/src/react-app-env.d.ts",
    "content": "/// <reference types=\"react-scripts\" />\n"
  },
  {
    "path": "examples/v1/counter-with-create-react-app-ts/frontend/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react\",\n    \"experimentalDecorators\": true,\n    \"skipLibCheck\": true\n  },\n  \"include\": [\n    \"src\"\n  ]\n}\n"
  },
  {
    "path": "examples/v1/counter-with-create-react-app-ts/package.json",
    "content": "{\n  \"name\": \"counter-with-create-react-app-ts\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"postinstall\": \"(cd ./frontend && npm install) && (cd ./backend && npm install)\",\n    \"start\": \"concurrently --names=frontend,backend --prefix-colors=green,blue --kill-others \\\"(cd ./frontend && npm run start)\\\" \\\"(cd ./backend && npm run start)\\\"\",\n    \"update\": \"(cd ./frontend && npm update) && (cd ./backend && npm update)\"\n  },\n  \"devDependencies\": {\n    \"concurrently\": \"^5.3.0\"\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter-with-esbuild-ts/README.md",
    "content": "# Counter with esbuild (TS)\n\nA simple example to introduce the core concepts of Layr.\n\n## Install\n\nInstall the npm dependencies with:\n\n```sh\nnpm install\n```\n\n## Usage\n\n### Running the app in development mode\n\nExecute the following command:\n\n```sh\nnpm run start\n```\n\nThe app should then be available at http://localhost:3000.\n\n### Debugging\n\n#### Client\n\nAdd the following entry in the local storage of your browser:\n\n```\n| Key   | Value     |\n| ----- | --------- |\n| debug | layr:* |\n```\n\n#### Server\n\nAdd the following environment variables when starting the app:\n\n```sh\nDEBUG=layr:* DEBUG_DEPTH=10\n```\n"
  },
  {
    "path": "examples/v1/counter-with-esbuild-ts/backend/package.json",
    "content": "{\n  \"name\": \"counter-with-esbuild-ts-backend\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"build\": \"esbuild ./src/http-server.ts --bundle --target=node12 --sourcemap --platform=node --external:koa --outfile=./build/bundle.js\",\n    \"start\": \"nodemon --watch ./src --ext ts --exec 'npm run build && node -r source-map-support/register ./build/bundle.js'\"\n  },\n  \"dependencies\": {\n    \"@layr/component\": \"^1.1.2\",\n    \"@layr/component-server\": \"^1.1.2\",\n    \"tslib\": \"^2.1.0\"\n  },\n  \"devDependencies\": {\n    \"@layr/component-http-server\": \"^1.1.2\",\n    \"@types/node\": \"^14.14.20\",\n    \"esbuild\": \"^0.9.2\",\n    \"nodemon\": \"^2.0.7\",\n    \"source-map-support\": \"^0.5.19\",\n    \"typescript\": \"^4.1.3\"\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter-with-esbuild-ts/backend/src/components/counter.ts",
    "content": "import {Component, primaryIdentifier, attribute, method, expose} from '@layr/component';\n\nexport class Counter extends Component {\n  @expose({get: true, set: true}) @primaryIdentifier() id!: string;\n\n  @expose({get: true, set: true}) @attribute('number') value = 0;\n\n  @expose({call: true}) @method() async increment() {\n    this.value++;\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter-with-esbuild-ts/backend/src/http-server.ts",
    "content": "import {ComponentHTTPServer} from '@layr/component-http-server';\n\nimport {server} from './server';\n\nconst httpServer = new ComponentHTTPServer(server, {port: 3001});\nhttpServer.start();\n"
  },
  {
    "path": "examples/v1/counter-with-esbuild-ts/backend/src/server.ts",
    "content": "import {ComponentServer} from '@layr/component-server';\n\nimport {Counter} from './components/counter';\n\nexport const server = new ComponentServer(Counter);\n"
  },
  {
    "path": "examples/v1/counter-with-esbuild-ts/backend/tsconfig.json",
    "content": "{\n  \"include\": [\"src/**/*\"],\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"module\": \"CommonJS\",\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"jsx\": \"react\",\n    \"sourceMap\": true,\n    \"importHelpers\": true,\n    \"strict\": true,\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"experimentalDecorators\": true,\n    \"resolveJsonModule\": true,\n    \"stripInternal\": true,\n    \"skipLibCheck\": true,\n    \"isolatedModules\": true\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter-with-esbuild-ts/frontend/package.json",
    "content": "{\n  \"name\": \"counter-with-esbuild-ts-frontend\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"copy\": \"mkdir -p ./build && cp ./src/index.html ./build/index.html\",\n    \"watch\": \"npm run copy && esbuild ./src/index.tsx --bundle --target=es2017 --watch --minify --keep-names --sourcemap --define:process.env.NODE_ENV=\\\\\\\"development\\\\\\\" --outfile=./build/bundle.js\",\n    \"serve\": \"serve --listen 3000 ./build\",\n    \"start\": \"npm-run-all --parallel watch serve\"\n  },\n  \"dependencies\": {\n    \"@layr/component\": \"^1.1.2\",\n    \"@layr/component-http-client\": \"^1.1.1\",\n    \"@layr/react-integration\": \"^1.0.22\",\n    \"react\": \"^16.13.1\",\n    \"react-dom\": \"^16.13.1\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^14.14.20\",\n    \"@types/react\": \"^16.14.2\",\n    \"@types/react-dom\": \"^16.9.10\",\n    \"esbuild\": \"^0.9.2\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"serve\": \"^11.3.2\"\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter-with-esbuild-ts/frontend/src/components/counter.tsx",
    "content": "import {ComponentHTTPClient} from '@layr/component-http-client';\nimport React from 'react';\nimport {view} from '@layr/react-integration';\n\nimport type {Counter as BackendCounter} from '../../../backend/src/components/counter';\n\nexport const getCounter = async () => {\n  const client = new ComponentHTTPClient('http://localhost:3001');\n\n  const BackendCounterProxy = (await client.getComponent()) as typeof BackendCounter;\n\n  class Counter extends BackendCounterProxy {\n    @view() Main() {\n      return (\n        <div>\n          <input value={this.value} readOnly />\n          <button\n            onClick={async () => {\n              await this.increment();\n            }}\n          >\n            +\n          </button>\n        </div>\n      );\n    }\n  }\n\n  return Counter;\n};\n"
  },
  {
    "path": "examples/v1/counter-with-esbuild-ts/frontend/src/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>My Rollup Project</title>\n  </head>\n\n  <body>\n    <div id=\"root\"></div>\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/v1/counter-with-esbuild-ts/frontend/src/index.tsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\n\nimport {getCounter} from './components/counter';\n\n(async () => {\n  const Counter = await getCounter();\n\n  const counter = new Counter();\n\n  ReactDOM.render(\n    <React.StrictMode>\n      <counter.Main />\n    </React.StrictMode>,\n    document.getElementById('root')\n  );\n})();\n"
  },
  {
    "path": "examples/v1/counter-with-esbuild-ts/frontend/tsconfig.json",
    "content": "{\n  \"include\": [\"src/**/*\"],\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"module\": \"CommonJS\",\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"jsx\": \"react\",\n    \"sourceMap\": true,\n    \"importHelpers\": true,\n    \"strict\": true,\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"experimentalDecorators\": true,\n    \"resolveJsonModule\": true,\n    \"stripInternal\": true,\n    \"skipLibCheck\": true,\n    \"isolatedModules\": true\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter-with-esbuild-ts/package.json",
    "content": "{\n  \"name\": \"counter-with-rollup-ts\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"postinstall\": \"(cd ./frontend && npm install) && (cd ./backend && npm install)\",\n    \"start\": \"concurrently --names=frontend,backend --prefix-colors=green,blue --kill-others \\\"(cd ./frontend && npm run start)\\\" \\\"(cd ./backend && npm run start)\\\"\",\n    \"update\": \"(cd ./frontend && npm update) && (cd ./backend && npm update)\"\n  },\n  \"devDependencies\": {\n    \"concurrently\": \"^5.3.0\"\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter-with-http/README.md",
    "content": "# Counter with HTTP\n\nA simple example to introduce the core concepts of Layr.\n\n## Usage\n\nInstall the npm dependencies with:\n\n```sh\nnpm install\n```\n\nRun the backend with:\n\n```sh\nnpx babel-node ./src/backend.js\n```\n\nThen, in another terminal, run the frontend with:\n\n```sh\nnpx babel-node ./src/frontend.js\n```\n"
  },
  {
    "path": "examples/v1/counter-with-http/babel.config.json",
    "content": "{\n  \"presets\": [[\"@babel/preset-env\", {\"targets\": {\"node\": \"10\"}}]],\n  \"plugins\": [\n    [\"@babel/plugin-proposal-decorators\", {\"legacy\": true}],\n    [\"@babel/plugin-proposal-class-properties\"]\n  ]\n}\n"
  },
  {
    "path": "examples/v1/counter-with-http/jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"module\": \"CommonJS\",\n    \"checkJs\": false,\n    \"experimentalDecorators\": true\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter-with-http/package.json",
    "content": "{\n  \"name\": \"counter-with-http\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A simple example to introduce the core concepts of Layr\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@layr/component\": \"^1.0.0\",\n    \"@layr/component-http-client\": \"^1.0.0\",\n    \"@layr/component-http-server\": \"^1.0.0\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.11.1\",\n    \"@babel/node\": \"^7.10.5\",\n    \"@babel/plugin-proposal-class-properties\": \"^7.10.4\",\n    \"@babel/plugin-proposal-decorators\": \"^7.10.5\",\n    \"@babel/preset-env\": \"^7.11.0\"\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter-with-http/src/backend.js",
    "content": "import {Component, primaryIdentifier, attribute, method, expose} from '@layr/component';\nimport {ComponentHTTPServer} from '@layr/component-http-server';\n\nclass Counter extends Component {\n  // We need a primary identifier so a Counter instance\n  // can be transported between the frontend and the backend\n  // while keeping it's identity\n  @expose({get: true, set: true}) @primaryIdentifier() id;\n\n  // The counter's value is exposed to the frontend\n  @expose({get: true, set: true}) @attribute('number') value = 0;\n\n  // And the \"business logic\" is exposed as well\n  @expose({call: true}) @method() increment() {\n    this.value++;\n  }\n}\n\n// We serve the Counter through a ComponentHTTPServer\nconst server = new ComponentHTTPServer(Counter, {port: 3210});\nserver.start();\n"
  },
  {
    "path": "examples/v1/counter-with-http/src/frontend.js",
    "content": "import {ComponentHTTPClient} from '@layr/component-http-client';\n\n(async () => {\n  // We create a client that is connected to the backend's server\n  const client = new ComponentHTTPClient('http://localhost:3210');\n\n  // We get the backend's Counter component\n  const BackendCounter = await client.getComponent();\n\n  // We extends the backend's Counter component so we can override the increment() method\n  class Counter extends BackendCounter {\n    async increment() {\n      await super.increment(); // The backend's `increment()` method is invoked\n      console.log(this.value); // Some additional code is executed in the frontend\n    }\n  }\n\n  // Lastly, we consume the Counter\n  const counter = new Counter();\n  await counter.increment();\n})();\n"
  },
  {
    "path": "examples/v1/counter-with-parcel-ts/README.md",
    "content": "# Counter with Parcel (TS)\n\nA simple example to introduce the core concepts of Layr.\n\n## Install\n\nInstall the npm dependencies with:\n\n```sh\nnpm install\n```\n\n## Usage\n\n### Running the app in development mode\n\nExecute the following command:\n\n```sh\nnpm run start\n```\n\nThe app should then be available at http://localhost:1234.\n\n### Debugging\n\n#### Client\n\nAdd the following entry in the local storage of your browser:\n\n```\n| Key   | Value     |\n| ----- | --------- |\n| debug | layr:* |\n```\n\n#### Server\n\nAdd the following environment variables when starting the app:\n\n```sh\nDEBUG=layr:* DEBUG_DEPTH=10\n```\n"
  },
  {
    "path": "examples/v1/counter-with-parcel-ts/backend/package.json",
    "content": "{\n  \"name\": \"counter-with-parcel-ts-backend\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"start\": \"nodemon --watch ./src --exec ts-node ./src/http-server.ts\"\n  },\n  \"dependencies\": {\n    \"@layr/component\": \"^1.1.2\",\n    \"@layr/component-server\": \"^1.1.2\",\n    \"tslib\": \"^2.1.0\"\n  },\n  \"devDependencies\": {\n    \"@layr/component-http-server\": \"^1.1.2\",\n    \"@types/node\": \"^14.14.20\",\n    \"nodemon\": \"^2.0.5\",\n    \"ts-node\": \"^9.1.1\",\n    \"typescript\": \"^4.1.3\"\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter-with-parcel-ts/backend/src/components/counter.ts",
    "content": "import {Component, primaryIdentifier, attribute, method, expose} from '@layr/component';\n\nexport class Counter extends Component {\n  @expose({get: true, set: true}) @primaryIdentifier() id!: string;\n\n  @expose({get: true, set: true}) @attribute('number') value = 0;\n\n  @expose({call: true}) @method() async increment() {\n    this.value++;\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter-with-parcel-ts/backend/src/http-server.ts",
    "content": "import {ComponentHTTPServer} from '@layr/component-http-server';\n\nimport {server} from './server';\n\nconst httpServer = new ComponentHTTPServer(server, {port: 1235});\nhttpServer.start();\n"
  },
  {
    "path": "examples/v1/counter-with-parcel-ts/backend/src/server.ts",
    "content": "import {ComponentServer} from '@layr/component-server';\n\nimport {Counter} from './components/counter';\n\nexport const server = new ComponentServer(Counter);\n"
  },
  {
    "path": "examples/v1/counter-with-parcel-ts/backend/tsconfig.json",
    "content": "{\n  \"include\": [\"src/**/*\"],\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"module\": \"CommonJS\",\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"jsx\": \"react\",\n    \"sourceMap\": true,\n    \"importHelpers\": true,\n    \"strict\": true,\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"experimentalDecorators\": true,\n    \"resolveJsonModule\": true,\n    \"stripInternal\": true,\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter-with-parcel-ts/frontend/.gitignore",
    "content": "/.parcel-cache\n"
  },
  {
    "path": "examples/v1/counter-with-parcel-ts/frontend/babel.config.js",
    "content": "module.exports = (api) => {\n  api.cache(true);\n\n  const presets = [\n    ['@babel/preset-typescript'],\n    [\n      '@babel/preset-env',\n      {\n        targets: {chrome: '55', safari: '11', firefox: '54'},\n        loose: true,\n        modules: false\n      }\n    ],\n    ['@babel/preset-react']\n  ];\n\n  const plugins = [\n    ['@babel/plugin-proposal-decorators', {legacy: true}],\n    ['@babel/plugin-proposal-class-properties', {loose: true}]\n  ];\n\n  return {presets, plugins};\n};\n"
  },
  {
    "path": "examples/v1/counter-with-parcel-ts/frontend/package.json",
    "content": "{\n  \"name\": \"counter-with-parcel-ts-frontend\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"start\": \"parcel serve ./src/index.html\"\n  },\n  \"dependencies\": {\n    \"@layr/component\": \"^1.1.2\",\n    \"@layr/component-http-client\": \"^1.1.1\",\n    \"@layr/react-integration\": \"^1.0.22\",\n    \"react\": \"^16.13.1\",\n    \"react-dom\": \"^16.13.1\"\n  },\n  \"devDependencies\": {\n    \"@babel/plugin-proposal-class-properties\": \"^7.10.4\",\n    \"@babel/plugin-proposal-decorators\": \"^7.12.12\",\n    \"@babel/preset-env\": \"^7.12.11\",\n    \"@babel/preset-react\": \"^7.12.10\",\n    \"@babel/preset-typescript\": \"^7.12.7\",\n    \"@types/node\": \"^14.14.20\",\n    \"@types/react\": \"^16.14.2\",\n    \"@types/react-dom\": \"^16.9.10\",\n    \"parcel\": \"next\"\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter-with-parcel-ts/frontend/src/components/counter.tsx",
    "content": "import {ComponentHTTPClient} from '@layr/component-http-client';\nimport React from 'react';\nimport {view} from '@layr/react-integration';\n\nimport type {Counter as BackendCounter} from '../../../backend/src/components/counter';\n\nexport const getCounter = async () => {\n  const client = new ComponentHTTPClient('http://localhost:1235');\n\n  const BackendCounterProxy = (await client.getComponent()) as typeof BackendCounter;\n\n  class Counter extends BackendCounterProxy {\n    @view() Main() {\n      return (\n        <div>\n          <input value={this.value} readOnly />\n          <button\n            onClick={async () => {\n              await this.increment();\n            }}\n          >\n            +\n          </button>\n        </div>\n      );\n    }\n  }\n\n  return Counter;\n};\n"
  },
  {
    "path": "examples/v1/counter-with-parcel-ts/frontend/src/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>My Parcel Project</title>\n  </head>\n\n  <body>\n    <div id=\"root\"></div>\n    <script src=\"./index.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/v1/counter-with-parcel-ts/frontend/src/index.tsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\n\nimport {getCounter} from './components/counter';\n\n(async () => {\n  const Counter = await getCounter();\n\n  const counter = new Counter();\n\n  ReactDOM.render(\n    <React.StrictMode>\n      <counter.Main />\n    </React.StrictMode>,\n    document.getElementById('root')\n  );\n})();\n"
  },
  {
    "path": "examples/v1/counter-with-parcel-ts/frontend/tsconfig.json",
    "content": "{\n  \"include\": [\"src/**/*\"],\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"module\": \"CommonJS\",\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"jsx\": \"react\",\n    \"sourceMap\": true,\n    \"importHelpers\": true,\n    \"strict\": true,\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"experimentalDecorators\": true,\n    \"resolveJsonModule\": true,\n    \"stripInternal\": true,\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter-with-parcel-ts/package.json",
    "content": "{\n  \"name\": \"counter-with-parcel-ts\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"postinstall\": \"(cd ./frontend && npm install) && (cd ./backend && npm install)\",\n    \"start\": \"concurrently --names=frontend,backend --prefix-colors=green,blue --kill-others \\\"(cd ./frontend && npm run start)\\\" \\\"(cd ./backend && npm run start)\\\"\",\n    \"update\": \"(cd ./frontend && npm update) && (cd ./backend && npm update)\"\n  },\n  \"devDependencies\": {\n    \"concurrently\": \"^5.3.0\"\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter-with-rollup-ts/README.md",
    "content": "# Counter with Rollup (TS)\n\nA simple example to introduce the core concepts of Layr.\n\n## Install\n\nInstall the npm dependencies with:\n\n```sh\nnpm install\n```\n\n## Usage\n\n### Running the app in development mode\n\nExecute the following command:\n\n```sh\nnpm run start\n```\n\nThe app should then be available at http://localhost:3000.\n\n### Debugging\n\n#### Client\n\nAdd the following entry in the local storage of your browser:\n\n```\n| Key   | Value     |\n| ----- | --------- |\n| debug | layr:* |\n```\n\n#### Server\n\nAdd the following environment variables when starting the app:\n\n```sh\nDEBUG=layr:* DEBUG_DEPTH=10\n```\n"
  },
  {
    "path": "examples/v1/counter-with-rollup-ts/backend/package.json",
    "content": "{\n  \"name\": \"counter-with-rollup-ts-backend\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"start\": \"nodemon --watch ./src --exec ts-node ./src/http-server.ts\"\n  },\n  \"dependencies\": {\n    \"@layr/component\": \"^1.1.2\",\n    \"@layr/component-server\": \"^1.1.2\",\n    \"tslib\": \"^2.1.0\"\n  },\n  \"devDependencies\": {\n    \"@layr/component-http-server\": \"^1.1.2\",\n    \"@types/node\": \"^14.14.20\",\n    \"nodemon\": \"^2.0.5\",\n    \"ts-node\": \"^9.1.1\",\n    \"typescript\": \"^4.1.3\"\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter-with-rollup-ts/backend/src/components/counter.ts",
    "content": "import {Component, primaryIdentifier, attribute, method, expose} from '@layr/component';\n\nexport class Counter extends Component {\n  @expose({get: true, set: true}) @primaryIdentifier() id!: string;\n\n  @expose({get: true, set: true}) @attribute('number') value = 0;\n\n  @expose({call: true}) @method() async increment() {\n    this.value++;\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter-with-rollup-ts/backend/src/http-server.ts",
    "content": "import {ComponentHTTPServer} from '@layr/component-http-server';\n\nimport {server} from './server';\n\nconst httpServer = new ComponentHTTPServer(server, {port: 3001});\nhttpServer.start();\n"
  },
  {
    "path": "examples/v1/counter-with-rollup-ts/backend/src/server.ts",
    "content": "import {ComponentServer} from '@layr/component-server';\n\nimport {Counter} from './components/counter';\n\nexport const server = new ComponentServer(Counter);\n"
  },
  {
    "path": "examples/v1/counter-with-rollup-ts/backend/tsconfig.json",
    "content": "{\n  \"include\": [\"src/**/*\"],\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"module\": \"CommonJS\",\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"jsx\": \"react\",\n    \"sourceMap\": true,\n    \"importHelpers\": true,\n    \"strict\": true,\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"experimentalDecorators\": true,\n    \"resolveJsonModule\": true,\n    \"stripInternal\": true,\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter-with-rollup-ts/frontend/babel.config.js",
    "content": "module.exports = (api) => {\n  api.cache(true);\n\n  const presets = [\n    ['@babel/preset-typescript'],\n    [\n      '@babel/preset-env',\n      {\n        targets: {chrome: '55', safari: '11', firefox: '54'},\n        loose: true,\n        modules: false\n      }\n    ],\n    ['@babel/preset-react']\n  ];\n\n  const plugins = [\n    ['@babel/plugin-proposal-decorators', {legacy: true}],\n    ['@babel/plugin-proposal-class-properties', {loose: true}]\n  ];\n\n  return {presets, plugins};\n};\n"
  },
  {
    "path": "examples/v1/counter-with-rollup-ts/frontend/package.json",
    "content": "{\n  \"name\": \"counter-with-rollup-ts-frontend\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"copy\": \"mkdir -p ./build && cp ./src/index.html ./build/index.html\",\n    \"build\": \"npm run copy && rollup --config\",\n    \"watch\": \"npm run copy && rollup --config --watch\",\n    \"serve\": \"serve --listen 3000 ./build\",\n    \"start\": \"npm-run-all --parallel watch serve\"\n  },\n  \"dependencies\": {\n    \"@layr/component\": \"^1.1.2\",\n    \"@layr/component-http-client\": \"^1.1.1\",\n    \"@layr/react-integration\": \"^1.0.22\",\n    \"react\": \"^16.13.1\",\n    \"react-dom\": \"^16.13.1\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.12.10\",\n    \"@babel/plugin-proposal-class-properties\": \"^7.10.4\",\n    \"@babel/plugin-proposal-decorators\": \"^7.12.12\",\n    \"@babel/preset-env\": \"^7.12.11\",\n    \"@babel/preset-react\": \"^7.12.10\",\n    \"@babel/preset-typescript\": \"^7.12.7\",\n    \"@rollup/plugin-babel\": \"^5.2.2\",\n    \"@rollup/plugin-commonjs\": \"^15.1.0\",\n    \"@rollup/plugin-node-resolve\": \"^9.0.0\",\n    \"@types/node\": \"^14.14.20\",\n    \"@types/react\": \"^16.14.2\",\n    \"@types/react-dom\": \"^16.9.10\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"rollup\": \"^2.36.0\",\n    \"rollup-plugin-node-globals\": \"^1.4.0\",\n    \"serve\": \"^11.3.2\"\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter-with-rollup-ts/frontend/rollup.config.js",
    "content": "import resolve from '@rollup/plugin-node-resolve';\nimport commonjs from '@rollup/plugin-commonjs';\nimport babel from '@rollup/plugin-babel';\nimport globals from 'rollup-plugin-node-globals';\n\nexport default {\n  input: 'src/index.tsx',\n  output: {\n    file: 'build/bundle.js',\n    format: 'iife',\n    sourcemap: true\n  },\n  plugins: [\n    resolve({browser: true, extensions: ['.mjs', '.js', '.json', '.node', '.ts', '.tsx']}),\n    commonjs(),\n    globals(),\n    babel({extensions: ['ts', 'tsx'], babelHelpers: 'bundled'})\n  ]\n};\n"
  },
  {
    "path": "examples/v1/counter-with-rollup-ts/frontend/src/components/counter.tsx",
    "content": "import {ComponentHTTPClient} from '@layr/component-http-client';\nimport React from 'react';\nimport {view} from '@layr/react-integration';\n\nimport type {Counter as BackendCounter} from '../../../backend/src/components/counter';\n\nexport const getCounter = async () => {\n  const client = new ComponentHTTPClient('http://localhost:3001');\n\n  const BackendCounterProxy = (await client.getComponent()) as typeof BackendCounter;\n\n  class Counter extends BackendCounterProxy {\n    @view() Main() {\n      return (\n        <div>\n          <input value={this.value} readOnly />\n          <button\n            onClick={async () => {\n              await this.increment();\n            }}\n          >\n            +\n          </button>\n        </div>\n      );\n    }\n  }\n\n  return Counter;\n};\n"
  },
  {
    "path": "examples/v1/counter-with-rollup-ts/frontend/src/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>My Rollup Project</title>\n  </head>\n\n  <body>\n    <div id=\"root\"></div>\n    <script src=\"bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/v1/counter-with-rollup-ts/frontend/src/index.tsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\n\nimport {getCounter} from './components/counter';\n\n(async () => {\n  const Counter = await getCounter();\n\n  const counter = new Counter();\n\n  ReactDOM.render(\n    <React.StrictMode>\n      <counter.Main />\n    </React.StrictMode>,\n    document.getElementById('root')\n  );\n})();\n"
  },
  {
    "path": "examples/v1/counter-with-rollup-ts/frontend/tsconfig.json",
    "content": "{\n  \"include\": [\"src/**/*\"],\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"module\": \"CommonJS\",\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"jsx\": \"react\",\n    \"sourceMap\": true,\n    \"importHelpers\": true,\n    \"strict\": true,\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"experimentalDecorators\": true,\n    \"resolveJsonModule\": true,\n    \"stripInternal\": true,\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "examples/v1/counter-with-rollup-ts/package.json",
    "content": "{\n  \"name\": \"counter-with-rollup-ts\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"postinstall\": \"(cd ./frontend && npm install) && (cd ./backend && npm install)\",\n    \"start\": \"concurrently --names=frontend,backend --prefix-colors=green,blue --kill-others \\\"(cd ./frontend && npm run start)\\\" \\\"(cd ./backend && npm run start)\\\"\",\n    \"update\": \"(cd ./frontend && npm update) && (cd ./backend && npm update)\"\n  },\n  \"devDependencies\": {\n    \"concurrently\": \"^5.3.0\"\n  }\n}\n"
  },
  {
    "path": "examples/v1/guestbook-cli-js/README.md",
    "content": "# Guestbook CLI (JS)\n\nA simple CLI app to introduce data storage with Layr.\n\nSee the [corresponding guide](https://layrjs.com/docs/v1/introduction/data-storage?language=js).\n"
  },
  {
    "path": "examples/v1/guestbook-cli-js/babel.config.json",
    "content": "{\n  \"presets\": [[\"@babel/preset-env\", {\"targets\": {\"node\": \"10\"}}]],\n  \"plugins\": [\n    [\"@babel/plugin-proposal-decorators\", {\"legacy\": true}],\n    [\"@babel/plugin-proposal-class-properties\"]\n  ]\n}\n"
  },
  {
    "path": "examples/v1/guestbook-cli-js/jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"module\": \"CommonJS\",\n    \"checkJs\": false,\n    \"experimentalDecorators\": true\n  }\n}\n"
  },
  {
    "path": "examples/v1/guestbook-cli-js/package.json",
    "content": "{\n  \"name\": \"guestbook-cli\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A simple CLI app to introduce data storage with Layr\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@layr/component\": \"^1.0.0\",\n    \"@layr/component-http-client\": \"^1.0.0\",\n    \"@layr/component-http-server\": \"^1.0.0\",\n    \"@layr/memory-store\": \"^1.0.0\",\n    \"@layr/storable\": \"^1.0.0\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.11.1\",\n    \"@babel/node\": \"^7.10.5\",\n    \"@babel/plugin-proposal-class-properties\": \"^7.10.4\",\n    \"@babel/plugin-proposal-decorators\": \"^7.10.5\",\n    \"@babel/preset-env\": \"^7.11.0\"\n  }\n}\n"
  },
  {
    "path": "examples/v1/guestbook-cli-js/src/backend.js",
    "content": "import {Component, expose, validators} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\nimport {MemoryStore} from '@layr/memory-store';\nimport {ComponentHTTPServer} from '@layr/component-http-server';\n\nconst {notEmpty, maxLength} = validators;\n\n@expose({\n  find: {call: true},\n  prototype: {\n    load: {call: true},\n    save: {call: true}\n  }\n})\nexport class Message extends Storable(Component) {\n  @expose({get: true, set: true}) @primaryIdentifier() id;\n\n  @expose({get: true, set: true})\n  @attribute('string', {validators: [notEmpty(), maxLength(300)]})\n  text = '';\n\n  @expose({get: true}) @attribute('Date') createdAt = new Date();\n}\n\nconst store = new MemoryStore();\n\nstore.registerStorable(Message);\n\nconst server = new ComponentHTTPServer(Message, {port: 3210});\n\nserver.start();\n"
  },
  {
    "path": "examples/v1/guestbook-cli-js/src/frontend.js",
    "content": "import {ComponentHTTPClient} from '@layr/component-http-client';\nimport {Storable} from '@layr/storable';\n\n(async () => {\n  const client = new ComponentHTTPClient('http://localhost:3210', {\n    mixins: [Storable]\n  });\n\n  const Message = await client.getComponent();\n\n  const text = process.argv[2];\n\n  if (text) {\n    addMessage(text);\n  } else {\n    showMessages();\n  }\n\n  async function addMessage(text) {\n    const message = new Message({text});\n    await message.save();\n    console.log(`Message successfully added`);\n  }\n\n  async function showMessages() {\n    const messages = await Message.find(\n      {},\n      {text: true, createdAt: true},\n      {sort: {createdAt: 'desc'}, limit: 30}\n    );\n\n    for (const message of messages) {\n      console.log(`[${message.createdAt.toISOString()}] ${message.text}`);\n    }\n  }\n})();\n"
  },
  {
    "path": "examples/v1/guestbook-cli-ts/README.md",
    "content": "# Guestbook CLI (TS)\n\nA simple CLI app to introduce data storage with Layr.\n\nSee the [corresponding guide](https://layrjs.com/docs/v1/introduction/data-storage?language=ts).\n"
  },
  {
    "path": "examples/v1/guestbook-cli-ts/package.json",
    "content": "{\n  \"name\": \"guestbook-cli\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A simple CLI app to introduce data storage with Layr\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@layr/component\": \"^1.0.0\",\n    \"@layr/component-http-client\": \"^1.0.0\",\n    \"@layr/component-http-server\": \"^1.0.0\",\n    \"@layr/memory-store\": \"^1.0.0\",\n    \"@layr/storable\": \"^1.0.0\"\n  },\n  \"devDependencies\": {\n    \"ts-node\": \"^8.10.2\",\n    \"typescript\": \"^3.9.7\"\n  }\n}\n"
  },
  {
    "path": "examples/v1/guestbook-cli-ts/src/backend.ts",
    "content": "import {Component, expose, validators} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\nimport {MemoryStore} from '@layr/memory-store';\nimport {ComponentHTTPServer} from '@layr/component-http-server';\n\nconst {notEmpty, maxLength} = validators;\n\n@expose({\n  find: {call: true},\n  prototype: {\n    load: {call: true},\n    save: {call: true}\n  }\n})\nexport class Message extends Storable(Component) {\n  @expose({get: true, set: true}) @primaryIdentifier() id!: string;\n\n  @expose({get: true, set: true})\n  @attribute('string', {validators: [notEmpty(), maxLength(300)]})\n  text = '';\n\n  @expose({get: true}) @attribute('Date') createdAt = new Date();\n}\n\nconst store = new MemoryStore();\n\nstore.registerStorable(Message);\n\nconst server = new ComponentHTTPServer(Message, {port: 3210});\n\nserver.start();\n"
  },
  {
    "path": "examples/v1/guestbook-cli-ts/src/frontend.ts",
    "content": "import {ComponentHTTPClient} from '@layr/component-http-client';\nimport {Storable} from '@layr/storable';\n\nimport type {Message as MessageType} from './backend';\n\n(async () => {\n  const client = new ComponentHTTPClient('http://localhost:3210', {\n    mixins: [Storable]\n  });\n\n  const Message = (await client.getComponent()) as typeof MessageType;\n\n  const text = process.argv[2];\n\n  if (text) {\n    addMessage(text);\n  } else {\n    showMessages();\n  }\n\n  async function addMessage(text: string) {\n    const message = new Message({text});\n    await message.save();\n    console.log(`Message successfully added`);\n  }\n\n  async function showMessages() {\n    const messages = await Message.find(\n      {},\n      {text: true, createdAt: true},\n      {sort: {createdAt: 'desc'}, limit: 30}\n    );\n\n    for (const message of messages) {\n      console.log(`[${message.createdAt.toISOString()}] ${message.text}`);\n    }\n  }\n})();\n"
  },
  {
    "path": "examples/v1/guestbook-cli-ts/tsconfig.json",
    "content": "{\n  \"include\": [\"src/**/*\"],\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"module\": \"CommonJS\",\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"sourceMap\": true,\n    \"strict\": true,\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"experimentalDecorators\": true,\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "examples/v1/guestbook-web-js/README.md",
    "content": "# Guestbook Web (JS)\n\nA simple web app showing how to build a web frontend with Layr.\n\nSee the [corresponding guide](https://layrjs.com/docs/v1/introduction/web-app?language=js).\n"
  },
  {
    "path": "examples/v1/guestbook-web-js/babel.config.json",
    "content": "{\n  \"presets\": [[\"@babel/preset-env\", {\"targets\": {\"node\": \"10\"}}], \"@babel/preset-react\"],\n  \"plugins\": [\n    [\"@babel/plugin-proposal-decorators\", {\"legacy\": true}],\n    [\"@babel/plugin-proposal-class-properties\"]\n  ]\n}\n"
  },
  {
    "path": "examples/v1/guestbook-web-js/jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"module\": \"CommonJS\",\n    \"checkJs\": false,\n    \"experimentalDecorators\": true\n  }\n}\n"
  },
  {
    "path": "examples/v1/guestbook-web-js/package.json",
    "content": "{\n  \"name\": \"guestbook-web\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A simple web app showing how to build a web frontend with Layr\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@layr/component\": \"^1.0.0\",\n    \"@layr/component-http-client\": \"^1.0.0\",\n    \"@layr/component-http-server\": \"^1.0.0\",\n    \"@layr/memory-store\": \"^1.0.0\",\n    \"@layr/react-integration\": \"^1.0.0\",\n    \"@layr/storable\": \"^1.0.0\",\n    \"react\": \"^16.13.1\",\n    \"react-dom\": \"^16.13.1\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.11.5\",\n    \"@babel/node\": \"^7.10.5\",\n    \"@babel/plugin-proposal-class-properties\": \"^7.10.4\",\n    \"@babel/plugin-proposal-decorators\": \"^7.10.5\",\n    \"@babel/preset-env\": \"^7.11.5\",\n    \"@babel/preset-react\": \"^7.10.4\",\n    \"babel-loader\": \"^8.1.0\",\n    \"html-webpack-plugin\": \"^4.4.1\",\n    \"webpack\": \"^4.44.1\",\n    \"webpack-cli\": \"^3.3.12\",\n    \"webpack-dev-server\": \"^3.11.0\"\n  }\n}\n"
  },
  {
    "path": "examples/v1/guestbook-web-js/src/backend.js",
    "content": "import {Component, expose, validators} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\nimport {MemoryStore} from '@layr/memory-store';\nimport {ComponentHTTPServer} from '@layr/component-http-server';\n\nconst {notEmpty, maxLength} = validators;\n\n@expose({\n  find: {call: true},\n  prototype: {\n    load: {call: true},\n    save: {call: true}\n  }\n})\nexport class Message extends Storable(Component) {\n  @expose({get: true, set: true}) @primaryIdentifier() id;\n\n  @expose({get: true, set: true})\n  @attribute('string', {validators: [notEmpty(), maxLength(300)]})\n  text = '';\n\n  @expose({get: true}) @attribute('Date') createdAt = new Date();\n}\n\nconst store = new MemoryStore();\n\nstore.registerStorable(Message);\n\nconst server = new ComponentHTTPServer(Message, {port: 3210});\n\nserver.start();\n"
  },
  {
    "path": "examples/v1/guestbook-web-js/src/frontend.js",
    "content": "import React, {useCallback} from 'react';\nimport ReactDOM from 'react-dom';\nimport {Component, attribute, provide} from '@layr/component';\nimport {Storable} from '@layr/storable';\nimport {ComponentHTTPClient} from '@layr/component-http-client';\nimport {view, useAsyncCall, useAsyncCallback, useRecomputableMemo} from '@layr/react-integration';\n\nasync function main() {\n  const client = new ComponentHTTPClient('http://localhost:3210', {\n    mixins: [Storable]\n  });\n\n  const BackendMessage = await client.getComponent();\n\n  class Message extends BackendMessage {\n    @view() Viewer() {\n      return (\n        <div>\n          <small>{this.createdAt.toLocaleString()}</small>\n          <br />\n          <strong>{this.text}</strong>\n        </div>\n      );\n    }\n\n    @view() Form({onSubmit}) {\n      const [handleSubmit, isSubmitting, submitError] = useAsyncCallback(async (event) => {\n        event.preventDefault();\n        await onSubmit();\n      });\n\n      return (\n        <form onSubmit={handleSubmit}>\n          <div>\n            <textarea\n              value={this.text}\n              onChange={(event) => {\n                this.text = event.target.value;\n              }}\n              required\n              style={{width: '100%', height: '80px'}}\n            />\n          </div>\n\n          <p>\n            <button type=\"submit\" disabled={isSubmitting}>\n              Submit\n            </button>\n          </p>\n\n          {submitError && (\n            <p style={{color: 'red'}}>Sorry, an error occurred while submitting your message.</p>\n          )}\n        </form>\n      );\n    }\n  }\n\n  class Guestbook extends Component {\n    @provide() static Message = Message;\n\n    @attribute('Message[]') static existingMessages = [];\n\n    @view() static Home() {\n      return (\n        <div style={{maxWidth: '700px', margin: '40px auto'}}>\n          <h1>Guestbook</h1>\n          <this.MessageList />\n          <this.MessageCreator />\n        </div>\n      );\n    }\n\n    @view() static MessageList() {\n      const {Message} = this;\n\n      const [isLoading, loadingError] = useAsyncCall(async () => {\n        this.existingMessages = await Message.find(\n          {},\n          {text: true, createdAt: true},\n          {sort: {createdAt: 'desc'}, limit: 30}\n        );\n      });\n\n      if (isLoading) {\n        return null;\n      }\n\n      if (loadingError) {\n        return (\n          <p style={{color: 'red'}}>\n            Sorry, an error occurred while loading the guestbook’s messages.\n          </p>\n        );\n      }\n\n      return (\n        <div>\n          <h2>All Messages</h2>\n          {this.existingMessages.length > 0 ? (\n            this.existingMessages.map((message) => (\n              <div key={message.id} style={{marginTop: '15px'}}>\n                <message.Viewer />\n              </div>\n            ))\n          ) : (\n            <p>No messages yet.</p>\n          )}\n        </div>\n      );\n    }\n\n    @view() static MessageCreator() {\n      const {Message} = this;\n\n      const [createdMessage, resetCreatedMessage] = useRecomputableMemo(() => new Message());\n\n      const saveMessage = useCallback(async () => {\n        await createdMessage.save();\n        this.existingMessages = [createdMessage, ...this.existingMessages];\n        resetCreatedMessage();\n      }, [createdMessage]);\n\n      return (\n        <div>\n          <h2>Add a Message</h2>\n          <createdMessage.Form onSubmit={saveMessage} />\n        </div>\n      );\n    }\n  }\n\n  ReactDOM.render(<Guestbook.Home />, document.getElementById('root'));\n}\n\nmain().catch((error) => console.error(error));\n"
  },
  {
    "path": "examples/v1/guestbook-web-js/src/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Guestbook</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta http-equiv=\"x-ua-compatible\" content=\"ie=edge\" />\n    <%= htmlWebpackPlugin.tags.headTags %>\n  </head>\n  <body>\n    <noscript><p>Sorry, this site requires JavaScript to be enabled.</p></noscript>\n    <div id=\"root\"></div>\n    <%= htmlWebpackPlugin.tags.bodyTags %>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/v1/guestbook-web-js/webpack.config.js",
    "content": "const webpack = require('webpack');\nconst HtmlWebPackPlugin = require('html-webpack-plugin');\nconst path = require('path');\n\nmodule.exports = (env, argv) => {\n  return {\n    // The entry point of the app is './src/frontend.js'\n    entry: './src/frontend.js',\n    output: {\n      // Specify '/' as the base path for all the assets\n      // This is required for a single-page application\n      publicPath: '/'\n    },\n    module: {\n      rules: [\n        {\n          // Use 'babel-loader' to compile the JS files\n          test: /\\.js$/,\n          include: path.join(__dirname, 'src'),\n          loader: 'babel-loader'\n        }\n      ]\n    },\n    plugins: [\n      // Use 'html-webpack-plugin' to generate the 'index.html' file\n      // from the './src/index.html' template\n      new HtmlWebPackPlugin({\n        template: './src/index.html',\n        inject: false\n      })\n    ],\n    // Generate source maps to make debugging easier\n    devtool: 'eval-cheap-module-source-map',\n    devServer: {\n      // Fallback to 'index.html' in case of 404 responses\n      // This is required for a single-page application\n      historyApiFallback: true\n    }\n  };\n};\n"
  },
  {
    "path": "examples/v1/guestbook-web-ts/README.md",
    "content": "# Guestbook Web (TS)\n\nA simple web app showing how to build a web frontend with Layr.\n\nSee the [corresponding guide](https://layrjs.com/docs/v1/introduction/web-app?language=ts).\n"
  },
  {
    "path": "examples/v1/guestbook-web-ts/package.json",
    "content": "{\n  \"name\": \"guestbook-web\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A simple web app showing how to build a web frontend with Layr\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@layr/component\": \"^1.0.0\",\n    \"@layr/component-http-client\": \"^1.0.0\",\n    \"@layr/component-http-server\": \"^1.0.0\",\n    \"@layr/memory-store\": \"^1.0.0\",\n    \"@layr/react-integration\": \"^1.0.0\",\n    \"@layr/storable\": \"^1.0.0\",\n    \"react\": \"^16.13.1\",\n    \"react-dom\": \"^16.13.1\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^16.9.49\",\n    \"@types/react-dom\": \"^16.9.8\",\n    \"html-webpack-plugin\": \"^4.3.0\",\n    \"ts-loader\": \"^7.0.5\",\n    \"ts-node\": \"^8.10.2\",\n    \"typescript\": \"^3.9.7\",\n    \"webpack\": \"^4.44.1\",\n    \"webpack-cli\": \"^3.3.12\",\n    \"webpack-dev-server\": \"^3.11.0\"\n  }\n}\n"
  },
  {
    "path": "examples/v1/guestbook-web-ts/src/backend.ts",
    "content": "import {Component, expose, validators} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\nimport {MemoryStore} from '@layr/memory-store';\nimport {ComponentHTTPServer} from '@layr/component-http-server';\n\nconst {notEmpty, maxLength} = validators;\n\n@expose({\n  find: {call: true},\n  prototype: {\n    load: {call: true},\n    save: {call: true}\n  }\n})\nexport class Message extends Storable(Component) {\n  @expose({get: true, set: true}) @primaryIdentifier() id!: string;\n\n  @expose({get: true, set: true})\n  @attribute('string', {validators: [notEmpty(), maxLength(300)]})\n  text = '';\n\n  @expose({get: true}) @attribute('Date') createdAt = new Date();\n}\n\nconst store = new MemoryStore();\n\nstore.registerStorable(Message);\n\nconst server = new ComponentHTTPServer(Message, {port: 3210});\n\nserver.start();\n"
  },
  {
    "path": "examples/v1/guestbook-web-ts/src/frontend.tsx",
    "content": "import React, {useCallback} from 'react';\nimport ReactDOM from 'react-dom';\nimport {Component, attribute, provide} from '@layr/component';\nimport {Storable} from '@layr/storable';\nimport {ComponentHTTPClient} from '@layr/component-http-client';\nimport {\n  view,\n  useAsyncCall,\n  useAsyncCallback,\n  useRecomputableMemo\n} from '@layr/react-integration';\n\nimport type {Message as MessageType} from './backend';\n\nasync function main() {\n  const client = new ComponentHTTPClient('http://localhost:3210', {\n    mixins: [Storable]\n  });\n\n  const BackendMessage = (await client.getComponent()) as typeof MessageType;\n\n  class Message extends BackendMessage {\n    @view() Viewer() {\n      return (\n        <div>\n          <small>{this.createdAt.toLocaleString()}</small>\n          <br />\n          <strong>{this.text}</strong>\n        </div>\n      );\n    }\n\n    @view() Form({onSubmit}: {onSubmit: () => Promise<void>}) {\n      const [handleSubmit, isSubmitting, submitError] = useAsyncCallback(async (event) => {\n        event.preventDefault();\n        await onSubmit();\n      });\n\n      return (\n        <form onSubmit={handleSubmit}>\n          <div>\n            <textarea\n              value={this.text}\n              onChange={(event) => {\n                this.text = event.target.value;\n              }}\n              required\n              style={{width: '100%', height: '80px'}}\n            />\n          </div>\n\n          <p>\n            <button type=\"submit\" disabled={isSubmitting}>\n              Submit\n            </button>\n          </p>\n\n          {submitError && (\n            <p style={{color: 'red'}}>Sorry, an error occurred while submitting your message.</p>\n          )}\n        </form>\n      );\n    }\n  }\n\n  class Guestbook extends Component {\n    @provide() static Message = Message;\n\n    @attribute('Message[]') static existingMessages: Message[] = [];\n\n    @view() static Home() {\n      return (\n        <div style={{maxWidth: '700px', margin: '40px auto'}}>\n          <h1>Guestbook</h1>\n          <this.MessageList />\n          <this.MessageCreator />\n        </div>\n      );\n    }\n\n    @view() static MessageList() {\n      const {Message} = this;\n\n      const [isLoading, loadingError] = useAsyncCall(async () => {\n        this.existingMessages = await Message.find(\n          {},\n          {text: true, createdAt: true},\n          {sort: {createdAt: 'desc'}, limit: 30}\n        );\n      });\n\n      if (isLoading) {\n        return null;\n      }\n\n      if (loadingError) {\n        return (\n          <p style={{color: 'red'}}>\n            Sorry, an error occurred while loading the guestbook’s messages.\n          </p>\n        );\n      }\n\n      return (\n        <div>\n          <h2>All Messages</h2>\n          {this.existingMessages.length > 0 ? (\n            this.existingMessages.map((message) => (\n              <div key={message.id} style={{marginTop: '15px'}}>\n                <message.Viewer />\n              </div>\n            ))\n          ) : (\n            <p>No messages yet.</p>\n          )}\n        </div>\n      );\n    }\n\n    @view() static MessageCreator() {\n      const {Message} = this;\n\n      const [createdMessage, resetCreatedMessage] = useRecomputableMemo(() => new Message());\n\n      const saveMessage = useCallback(async () => {\n        await createdMessage.save();\n        this.existingMessages = [createdMessage, ...this.existingMessages];\n        resetCreatedMessage();\n      }, [createdMessage]);\n\n      return (\n        <div>\n          <h2>Add a Message</h2>\n          <createdMessage.Form onSubmit={saveMessage} />\n        </div>\n      );\n    }\n  }\n\n  ReactDOM.render(<Guestbook.Home />, document.getElementById('root'));\n}\n\nmain().catch((error) => console.error(error));\n"
  },
  {
    "path": "examples/v1/guestbook-web-ts/src/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Guestbook</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta http-equiv=\"x-ua-compatible\" content=\"ie=edge\" />\n    <%= htmlWebpackPlugin.tags.headTags %>\n  </head>\n  <body>\n    <noscript><p>Sorry, this site requires JavaScript to be enabled.</p></noscript>\n    <div id=\"root\"></div>\n    <%= htmlWebpackPlugin.tags.bodyTags %>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/v1/guestbook-web-ts/tsconfig.json",
    "content": "{\n  \"include\": [\"src/**/*\"],\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"module\": \"CommonJS\",\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"jsx\": \"react\",\n    \"sourceMap\": true,\n    \"strict\": true,\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"experimentalDecorators\": true,\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "examples/v1/guestbook-web-ts/webpack.config.js",
    "content": "const webpack = require('webpack');\nconst HtmlWebPackPlugin = require('html-webpack-plugin');\nconst path = require('path');\n\nmodule.exports = (env, argv) => {\n  return {\n    // The entry point of the app is './src/frontend.tsx'\n    entry: './src/frontend.tsx',\n    output: {\n      // Specify '/' as the base path for all the assets\n      // This is required for a single-page application\n      publicPath: '/'\n    },\n    module: {\n      rules: [\n        {\n          // Use 'ts-loader' to compile the TS files\n          test: /\\.tsx?$/,\n          include: path.join(__dirname, 'src'),\n          loader: 'ts-loader'\n        }\n      ]\n    },\n    plugins: [\n      // Use 'html-webpack-plugin' to generate the 'index.html' file\n      // from the './src/index.html' template\n      new HtmlWebPackPlugin({\n        template: './src/index.html',\n        inject: false\n      })\n    ],\n    // Generate source maps to make debugging easier\n    devtool: 'eval-cheap-module-source-map',\n    devServer: {\n      // Fallback to 'index.html' in case of 404 responses\n      // This is required for a single-page application\n      historyApiFallback: true\n    }\n  };\n};\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-authorization-js/README.md",
    "content": "# Guestbook Web With Authorization (JS)\n\nA simple web app showing how to build a web frontend with authorization with Layr.\n\nSee the [corresponding guide](https://layrjs.com/docs/v1/introduction/authorization?language=js).\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-authorization-js/babel.config.json",
    "content": "{\n  \"presets\": [[\"@babel/preset-env\", {\"targets\": {\"node\": \"10\"}}], \"@babel/preset-react\"],\n  \"plugins\": [\n    [\"@babel/plugin-proposal-decorators\", {\"legacy\": true}],\n    [\"@babel/plugin-proposal-class-properties\"]\n  ]\n}\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-authorization-js/jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"module\": \"CommonJS\",\n    \"checkJs\": false,\n    \"experimentalDecorators\": true\n  }\n}\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-authorization-js/package.json",
    "content": "{\n  \"name\": \"guestbook-web-with-authorization\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A simple web app showing how to build a web frontend with authorization with Layr\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@layr/component\": \"^1.0.0\",\n    \"@layr/component-http-client\": \"^1.0.0\",\n    \"@layr/component-http-server\": \"^1.0.0\",\n    \"@layr/memory-store\": \"^1.0.0\",\n    \"@layr/react-integration\": \"^1.0.0\",\n    \"@layr/routable\": \"^1.0.0\",\n    \"@layr/storable\": \"^1.0.0\",\n    \"@layr/with-roles\": \"^1.0.0\",\n    \"react\": \"^16.13.1\",\n    \"react-dom\": \"^16.13.1\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.11.1\",\n    \"@babel/node\": \"^7.10.5\",\n    \"@babel/plugin-proposal-class-properties\": \"^7.10.4\",\n    \"@babel/plugin-proposal-decorators\": \"^7.10.5\",\n    \"@babel/preset-env\": \"^7.11.0\",\n    \"@babel/preset-react\": \"^7.10.4\",\n    \"babel-loader\": \"^8.1.0\",\n    \"html-webpack-plugin\": \"^4.3.0\",\n    \"webpack\": \"^4.44.1\",\n    \"webpack-cli\": \"^3.3.12\",\n    \"webpack-dev-server\": \"^3.11.0\"\n  }\n}\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-authorization-js/src/backend.js",
    "content": "import {Component, provide, consume, expose, validators} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\nimport {WithRoles, role} from '@layr/with-roles';\nimport {MemoryStore} from '@layr/memory-store';\nimport {ComponentHTTPServer} from '@layr/component-http-server';\n\nconst {notEmpty, maxLength} = validators;\n\nexport class Session extends Component {\n  @expose({set: true})\n  @attribute('string?')\n  static secret;\n\n  static isAdmin() {\n    return this.secret === process.env.ADMIN_SECRET;\n  }\n}\n\n@expose({\n  find: {call: true},\n  prototype: {\n    load: {call: true},\n    save: {call: ['creator', 'admin']}\n  }\n})\nexport class Message extends WithRoles(Storable(Component)) {\n  @provide() static Session = Session;\n\n  @expose({get: true, set: true}) @primaryIdentifier() id;\n\n  @expose({get: true, set: true})\n  @attribute('string', {validators: [notEmpty(), maxLength(300)]})\n  text = '';\n\n  @expose({get: true}) @attribute('Date') createdAt = new Date();\n\n  @role('creator') creatorRoleResolver() {\n    return this.isNew();\n  }\n\n  @role('admin') static adminRoleResolver() {\n    return this.Session.isAdmin();\n  }\n}\n\nconst store = new MemoryStore();\n\nstore.registerStorable(Message);\n\nconst server = new ComponentHTTPServer(Message, {port: 3210});\n\nserver.start();\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-authorization-js/src/frontend.js",
    "content": "import React, {useCallback} from 'react';\nimport ReactDOM from 'react-dom';\nimport {Component, attribute, provide} from '@layr/component';\nimport {Storable} from '@layr/storable';\nimport {Routable, route} from '@layr/routable';\nimport {ComponentHTTPClient} from '@layr/component-http-client';\nimport {\n  view,\n  useBrowserRouter,\n  useAsyncCall,\n  useAsyncCallback,\n  useAsyncMemo,\n  useRecomputableMemo\n} from '@layr/react-integration';\n\nasync function main() {\n  const client = new ComponentHTTPClient('http://localhost:3210', {\n    mixins: [Storable]\n  });\n\n  const BackendMessage = await client.getComponent();\n\n  class Session extends BackendMessage.Session {\n    @attribute('string?', {\n      getter() {\n        return window.localStorage.getItem('secret') || undefined;\n      }\n    })\n    static secret;\n  }\n\n  class Message extends BackendMessage {\n    @provide() static Session = Session;\n\n    @view() Viewer() {\n      return (\n        <div>\n          <small>{this.createdAt.toLocaleString()}</small>\n          <br />\n          <strong>{this.text}</strong>\n        </div>\n      );\n    }\n\n    @view() Form({onSubmit}) {\n      const [handleSubmit, isSubmitting, submitError] = useAsyncCallback(async (event) => {\n        event.preventDefault();\n        await onSubmit();\n      });\n\n      return (\n        <form onSubmit={handleSubmit}>\n          <div>\n            <textarea\n              value={this.text}\n              onChange={(event) => {\n                this.text = event.target.value;\n              }}\n              required\n              style={{width: '100%', height: '80px'}}\n            />\n          </div>\n\n          <p>\n            <button type=\"submit\" disabled={isSubmitting}>\n              Submit\n            </button>\n          </p>\n\n          {submitError && (\n            <p style={{color: 'red'}}>Sorry, an error occurred while submitting your message.</p>\n          )}\n        </form>\n      );\n    }\n  }\n\n  class Guestbook extends Routable(Component) {\n    @provide() static Message = Message;\n\n    @attribute('Message[]') static existingMessages = [];\n\n    @view() static Root() {\n      const [router, isReady] = useBrowserRouter(this);\n\n      if (!isReady) {\n        return null;\n      }\n\n      const content = router.callCurrentRoute({fallback: () => 'Sorry, there is nothing here.'});\n\n      return (\n        <div style={{maxWidth: '700px', margin: '40px auto'}}>\n          <h1>Guestbook</h1>\n          {content}\n        </div>\n      );\n    }\n\n    @route('/') @view() static Home() {\n      return (\n        <div>\n          <this.MessageList />\n          <this.MessageCreator />\n        </div>\n      );\n    }\n\n    @view() static MessageList() {\n      const {Message} = this;\n\n      const [isLoading, loadingError] = useAsyncCall(async () => {\n        this.existingMessages = await Message.find(\n          {},\n          {text: true, createdAt: true},\n          {sort: {createdAt: 'desc'}, limit: 30}\n        );\n      });\n\n      if (isLoading) {\n        return null;\n      }\n\n      if (loadingError) {\n        return (\n          <p style={{color: 'red'}}>\n            Sorry, an error occurred while loading the guestbook’s messages.\n          </p>\n        );\n      }\n\n      return (\n        <div>\n          <h2>All Messages</h2>\n          {this.existingMessages.length > 0 ? (\n            this.existingMessages.map((message) => (\n              <div key={message.id} style={{marginTop: '15px'}}>\n                <message.Viewer />\n                {Message.Session.secret && (\n                  <div style={{marginTop: '5px'}}>\n                    <this.MessageEditor.Link params={message}>Edit</this.MessageEditor.Link>\n                  </div>\n                )}\n              </div>\n            ))\n          ) : (\n            <p>No messages yet.</p>\n          )}\n        </div>\n      );\n    }\n\n    @view() static MessageCreator() {\n      const {Message} = this;\n\n      const [createdMessage, resetCreatedMessage] = useRecomputableMemo(() => new Message());\n\n      const saveMessage = useCallback(async () => {\n        await createdMessage.save();\n        this.existingMessages = [createdMessage, ...this.existingMessages];\n        resetCreatedMessage();\n      }, [createdMessage]);\n\n      return (\n        <div>\n          <h2>Add a Message</h2>\n          <createdMessage.Form onSubmit={saveMessage} />\n        </div>\n      );\n    }\n\n    @route('/messages/:id') @view() static MessageEditor({id}) {\n      const {Message} = this;\n\n      const [\n        {existingMessage, editedMessage} = {},\n        isLoading,\n        loadingError\n      ] = useAsyncMemo(async () => {\n        const existingMessage = await Message.get(id, {text: true});\n        const editedMessage = existingMessage.fork();\n        return {existingMessage, editedMessage};\n      }, [id]);\n\n      const saveMessage = useCallback(async () => {\n        await editedMessage.save();\n        existingMessage.merge(editedMessage);\n        this.Home.navigate();\n      }, [existingMessage, editedMessage]);\n\n      if (isLoading) {\n        return null;\n      }\n\n      if (loadingError) {\n        return (\n          <p style={{color: 'red'}}>\n            Sorry, an error occurred while loading a guestbook’s message.\n          </p>\n        );\n      }\n\n      return (\n        <div>\n          <h2>Edit a Message</h2>\n          <editedMessage.Form onSubmit={saveMessage} />\n        </div>\n      );\n    }\n  }\n\n  ReactDOM.render(<Guestbook.Root />, document.getElementById('root'));\n}\n\nmain().catch((error) => console.error(error));\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-authorization-js/src/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Guestbook</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta http-equiv=\"x-ua-compatible\" content=\"ie=edge\" />\n    <%= htmlWebpackPlugin.tags.headTags %>\n  </head>\n  <body>\n    <noscript><p>Sorry, this site requires JavaScript to be enabled.</p></noscript>\n    <div id=\"root\"></div>\n    <%= htmlWebpackPlugin.tags.bodyTags %>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-authorization-js/webpack.config.js",
    "content": "const webpack = require('webpack');\nconst HtmlWebPackPlugin = require('html-webpack-plugin');\nconst path = require('path');\n\nmodule.exports = (env, argv) => {\n  return {\n    // The entry point of the app is './src/frontend.js'\n    entry: './src/frontend.js',\n    output: {\n      // Specify '/' as the base path for all the assets\n      // This is required for a single-page application\n      publicPath: '/'\n    },\n    module: {\n      rules: [\n        {\n          // Use 'babel-loader' to compile the JS files\n          test: /\\.js$/,\n          include: path.join(__dirname, 'src'),\n          loader: 'babel-loader'\n        }\n      ]\n    },\n    plugins: [\n      // Use 'html-webpack-plugin' to generate the 'index.html' file\n      // from the './src/index.html' template\n      new HtmlWebPackPlugin({\n        template: './src/index.html',\n        inject: false\n      })\n    ],\n    // Generate source maps to make debugging easier\n    devtool: 'eval-cheap-module-source-map',\n    devServer: {\n      // Fallback to 'index.html' in case of 404 responses\n      // This is required for a single-page application\n      historyApiFallback: true\n    }\n  };\n};\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-authorization-ts/README.md",
    "content": "# Guestbook Web With Authorization (TS)\n\nA simple web app showing how to build a web frontend with authorization with Layr.\n\nSee the [corresponding guide](https://layrjs.com/docs/v1/introduction/authorization?language=ts).\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-authorization-ts/package.json",
    "content": "{\n  \"name\": \"guestbook-web-with-authorization\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A simple web app showing how to build a web frontend with authorization with Layr\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@layr/component\": \"^1.0.0\",\n    \"@layr/component-http-client\": \"^1.0.0\",\n    \"@layr/component-http-server\": \"^1.0.0\",\n    \"@layr/memory-store\": \"^1.0.0\",\n    \"@layr/react-integration\": \"^1.0.0\",\n    \"@layr/routable\": \"^1.0.0\",\n    \"@layr/storable\": \"^1.0.0\",\n    \"@layr/with-roles\": \"^1.0.0\",\n    \"react\": \"^16.13.1\",\n    \"react-dom\": \"^16.13.1\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^16.9.36\",\n    \"@types/react-dom\": \"^16.9.8\",\n    \"html-webpack-plugin\": \"^4.3.0\",\n    \"ts-loader\": \"^7.0.5\",\n    \"ts-node\": \"^8.10.2\",\n    \"typescript\": \"^3.9.7\",\n    \"webpack\": \"^4.44.1\",\n    \"webpack-cli\": \"^3.3.12\",\n    \"webpack-dev-server\": \"^3.11.0\"\n  }\n}\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-authorization-ts/src/backend.ts",
    "content": "import {Component, provide, expose, validators} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\nimport {WithRoles, role} from '@layr/with-roles';\nimport {MemoryStore} from '@layr/memory-store';\nimport {ComponentHTTPServer} from '@layr/component-http-server';\n\nconst {notEmpty, maxLength} = validators;\n\nexport class Session extends Component {\n  @expose({set: true})\n  @attribute('string?')\n  static secret?: string;\n\n  static isAdmin() {\n    return this.secret === process.env.ADMIN_SECRET;\n  }\n}\n\n@expose({\n  find: {call: true},\n  prototype: {\n    load: {call: true},\n    save: {call: ['creator', 'admin']}\n  }\n})\nexport class Message extends WithRoles(Storable(Component)) {\n  @provide() static Session = Session;\n\n  @expose({get: true, set: true}) @primaryIdentifier() id!: string;\n\n  @expose({get: true, set: true})\n  @attribute('string', {validators: [notEmpty(), maxLength(300)]})\n  text = '';\n\n  @expose({get: true}) @attribute('Date') createdAt = new Date();\n\n  @role('creator') creatorRoleResolver() {\n    return this.isNew();\n  }\n\n  @role('admin') static adminRoleResolver() {\n    return this.Session.isAdmin();\n  }\n}\n\nconst store = new MemoryStore();\n\nstore.registerStorable(Message);\n\nconst server = new ComponentHTTPServer(Message, {port: 3210});\n\nserver.start();\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-authorization-ts/src/frontend.tsx",
    "content": "import React, {useCallback} from 'react';\nimport ReactDOM from 'react-dom';\nimport {Component, provide, attribute} from '@layr/component';\nimport {Storable} from '@layr/storable';\nimport {Routable, route} from '@layr/routable';\nimport {ComponentHTTPClient} from '@layr/component-http-client';\nimport {\n  view,\n  useBrowserRouter,\n  useAsyncCall,\n  useAsyncCallback,\n  useAsyncMemo,\n  useRecomputableMemo\n} from '@layr/react-integration';\n\nimport type {Message as MessageType} from './backend';\n\nasync function main() {\n  const client = new ComponentHTTPClient('http://localhost:3210', {\n    mixins: [Storable]\n  });\n\n  const BackendMessage = (await client.getComponent()) as typeof MessageType;\n\n  class Session extends BackendMessage.Session {\n    @attribute('string?', {\n      getter() {\n        return window.localStorage.getItem('secret') || undefined;\n      }\n    })\n    static secret?: string;\n  }\n\n  class Message extends BackendMessage {\n    @provide() static Session = Session;\n\n    @view() Viewer() {\n      return (\n        <div>\n          <small>{this.createdAt.toLocaleString()}</small>\n          <br />\n          <strong>{this.text}</strong>\n        </div>\n      );\n    }\n\n    @view() Form({onSubmit}: {onSubmit: () => Promise<void>}) {\n      const [handleSubmit, isSubmitting, submitError] = useAsyncCallback(async (event) => {\n        event.preventDefault();\n        await onSubmit();\n      });\n\n      return (\n        <form onSubmit={handleSubmit}>\n          <div>\n            <textarea\n              value={this.text}\n              onChange={(event) => {\n                this.text = event.target.value;\n              }}\n              required\n              style={{width: '100%', height: '80px'}}\n            />\n          </div>\n\n          <p>\n            <button type=\"submit\" disabled={isSubmitting}>\n              Submit\n            </button>\n          </p>\n\n          {submitError && (\n            <p style={{color: 'red'}}>Sorry, an error occurred while submitting your message.</p>\n          )}\n        </form>\n      );\n    }\n  }\n\n  class Guestbook extends Routable(Component) {\n    @provide() static Message = Message;\n\n    @attribute('Message[]') static existingMessages: Message[] = [];\n\n    @view() static Root() {\n      const [router, isReady] = useBrowserRouter(this);\n\n      if (!isReady) {\n        return null;\n      }\n\n      const content = router.callCurrentRoute({fallback: () => 'Sorry, there is nothing here.'});\n\n      return (\n        <div style={{maxWidth: '700px', margin: '40px auto'}}>\n          <h1>Guestbook</h1>\n          {content}\n        </div>\n      );\n    }\n\n    @route('/') @view() static Home() {\n      return (\n        <div>\n          <this.MessageList />\n          <this.MessageCreator />\n        </div>\n      );\n    }\n\n    @view() static MessageList() {\n      const {Message} = this;\n\n      const [isLoading, loadingError] = useAsyncCall(async () => {\n        this.existingMessages = await Message.find(\n          {},\n          {text: true, createdAt: true},\n          {sort: {createdAt: 'desc'}, limit: 30}\n        );\n      });\n\n      if (isLoading) {\n        return null;\n      }\n\n      if (loadingError) {\n        return (\n          <p style={{color: 'red'}}>\n            Sorry, an error occurred while loading the guestbook’s messages.\n          </p>\n        );\n      }\n\n      return (\n        <div>\n          <h2>All Messages</h2>\n          {this.existingMessages.length > 0 ? (\n            this.existingMessages.map((message) => (\n              <div key={message.id} style={{marginTop: '15px'}}>\n                <message.Viewer />\n                {Message.Session.secret && (\n                  <div style={{marginTop: '5px'}}>\n                    <this.MessageEditor.Link params={message}>Edit</this.MessageEditor.Link>\n                  </div>\n                )}\n              </div>\n            ))\n          ) : (\n            <p>No messages yet.</p>\n          )}\n        </div>\n      );\n    }\n\n    @view() static MessageCreator() {\n      const {Message} = this;\n\n      const [createdMessage, resetCreatedMessage] = useRecomputableMemo(() => new Message());\n\n      const saveMessage = useCallback(async () => {\n        await createdMessage.save();\n        this.existingMessages = [createdMessage, ...this.existingMessages];\n        resetCreatedMessage();\n      }, [createdMessage]);\n\n      return (\n        <div>\n          <h2>Add a Message</h2>\n          <createdMessage.Form onSubmit={saveMessage} />\n        </div>\n      );\n    }\n\n    @route('/messages/:id') @view() static MessageEditor({id}: {id: string}) {\n      const {Message} = this;\n\n      const [{existingMessage, editedMessage} = {} as const, isLoading] = useAsyncMemo(async () => {\n        const existingMessage = await Message.get(id, {text: true});\n        const editedMessage = existingMessage.fork();\n        return {existingMessage, editedMessage};\n      }, [id]);\n\n      const saveMessage = useCallback(async () => {\n        await editedMessage!.save();\n        existingMessage!.merge(editedMessage!);\n        this.Home.navigate();\n      }, [existingMessage, editedMessage]);\n\n      if (isLoading) {\n        return null;\n      }\n\n      if (editedMessage === undefined) {\n        return (\n          <p style={{color: 'red'}}>\n            Sorry, an error occurred while loading a guestbook’s message.\n          </p>\n        );\n      }\n\n      return (\n        <div>\n          <h2>Edit a Message</h2>\n          <editedMessage.Form onSubmit={saveMessage} />\n        </div>\n      );\n    }\n  }\n\n  ReactDOM.render(<Guestbook.Root />, document.getElementById('root'));\n}\n\nmain().catch((error) => console.error(error));\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-authorization-ts/src/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Guestbook</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta http-equiv=\"x-ua-compatible\" content=\"ie=edge\" />\n    <%= htmlWebpackPlugin.tags.headTags %>\n  </head>\n  <body>\n    <noscript><p>Sorry, this site requires JavaScript to be enabled.</p></noscript>\n    <div id=\"root\"></div>\n    <%= htmlWebpackPlugin.tags.bodyTags %>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-authorization-ts/tsconfig.json",
    "content": "{\n  \"include\": [\"src/**/*\"],\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"module\": \"CommonJS\",\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"jsx\": \"react\",\n    \"sourceMap\": true,\n    \"strict\": true,\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"experimentalDecorators\": true,\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-authorization-ts/webpack.config.js",
    "content": "const webpack = require('webpack');\nconst HtmlWebPackPlugin = require('html-webpack-plugin');\nconst path = require('path');\n\nmodule.exports = (env, argv) => {\n  return {\n    // The entry point of the app is './src/frontend.tsx'\n    entry: './src/frontend.tsx',\n    output: {\n      // Specify '/' as the base path for all the assets\n      // This is required for a single-page application\n      publicPath: '/'\n    },\n    module: {\n      rules: [\n        {\n          // Use 'ts-loader' to compile the TS files\n          test: /\\.tsx?$/,\n          include: path.join(__dirname, 'src'),\n          loader: 'ts-loader'\n        }\n      ]\n    },\n    plugins: [\n      // Use 'html-webpack-plugin' to generate the 'index.html' file\n      // from the './src/index.html' template\n      new HtmlWebPackPlugin({\n        template: './src/index.html',\n        inject: false\n      })\n    ],\n    // Generate source maps to make debugging easier\n    devtool: 'eval-cheap-module-source-map',\n    devServer: {\n      // Fallback to 'index.html' in case of 404 responses\n      // This is required for a single-page application\n      historyApiFallback: true\n    }\n  };\n};\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-routes-js/README.md",
    "content": "# Guestbook Web With Routes (JS)\n\nA simple web app showing how to build a web frontend using routes with Layr.\n\nSee the [corresponding guide](https://layrjs.com/docs/v1/introduction/routing?language=js).\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-routes-js/babel.config.json",
    "content": "{\n  \"presets\": [[\"@babel/preset-env\", {\"targets\": {\"node\": \"10\"}}], \"@babel/preset-react\"],\n  \"plugins\": [\n    [\"@babel/plugin-proposal-decorators\", {\"legacy\": true}],\n    [\"@babel/plugin-proposal-class-properties\"]\n  ]\n}\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-routes-js/jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"module\": \"CommonJS\",\n    \"checkJs\": false,\n    \"experimentalDecorators\": true\n  }\n}\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-routes-js/package.json",
    "content": "{\n  \"name\": \"guestbook-web-with-routes\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A simple web app showing how to build a web frontend using routes with Layr\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@layr/component\": \"^1.0.0\",\n    \"@layr/component-http-client\": \"^1.0.0\",\n    \"@layr/component-http-server\": \"^1.0.0\",\n    \"@layr/memory-store\": \"^1.0.0\",\n    \"@layr/react-integration\": \"^1.0.0\",\n    \"@layr/routable\": \"^1.0.0\",\n    \"@layr/storable\": \"^1.0.0\",\n    \"react\": \"^16.13.1\",\n    \"react-dom\": \"^16.13.1\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.11.1\",\n    \"@babel/node\": \"^7.10.5\",\n    \"@babel/plugin-proposal-class-properties\": \"^7.10.4\",\n    \"@babel/plugin-proposal-decorators\": \"^7.10.5\",\n    \"@babel/preset-env\": \"^7.11.0\",\n    \"@babel/preset-react\": \"^7.10.4\",\n    \"babel-loader\": \"^8.1.0\",\n    \"html-webpack-plugin\": \"^4.3.0\",\n    \"webpack\": \"^4.44.1\",\n    \"webpack-cli\": \"^3.3.12\",\n    \"webpack-dev-server\": \"^3.11.0\"\n  }\n}\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-routes-js/src/backend.js",
    "content": "import {Component, expose, validators} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\nimport {MemoryStore} from '@layr/memory-store';\nimport {ComponentHTTPServer} from '@layr/component-http-server';\n\nconst {notEmpty, maxLength} = validators;\n\n@expose({\n  find: {call: true},\n  prototype: {\n    load: {call: true},\n    save: {call: true}\n  }\n})\nexport class Message extends Storable(Component) {\n  @expose({get: true, set: true}) @primaryIdentifier() id;\n\n  @expose({get: true, set: true})\n  @attribute('string', {validators: [notEmpty(), maxLength(300)]})\n  text = '';\n\n  @expose({get: true}) @attribute('Date') createdAt = new Date();\n}\n\nconst store = new MemoryStore();\n\nstore.registerStorable(Message);\n\nconst server = new ComponentHTTPServer(Message, {port: 3210});\n\nserver.start();\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-routes-js/src/frontend.js",
    "content": "import React, {useCallback} from 'react';\nimport ReactDOM from 'react-dom';\nimport {Component, attribute, provide} from '@layr/component';\nimport {Storable} from '@layr/storable';\nimport {Routable, route} from '@layr/routable';\nimport {ComponentHTTPClient} from '@layr/component-http-client';\nimport {\n  view,\n  useBrowserRouter,\n  useAsyncCall,\n  useAsyncCallback,\n  useAsyncMemo,\n  useRecomputableMemo\n} from '@layr/react-integration';\n\nasync function main() {\n  const client = new ComponentHTTPClient('http://localhost:3210', {\n    mixins: [Storable]\n  });\n\n  const BackendMessage = await client.getComponent();\n\n  class Message extends BackendMessage {\n    @view() Viewer() {\n      return (\n        <div>\n          <small>{this.createdAt.toLocaleString()}</small>\n          <br />\n          <strong>{this.text}</strong>\n        </div>\n      );\n    }\n\n    @view() Form({onSubmit}) {\n      const [handleSubmit, isSubmitting, submitError] = useAsyncCallback(async (event) => {\n        event.preventDefault();\n        await onSubmit();\n      });\n\n      return (\n        <form onSubmit={handleSubmit}>\n          <div>\n            <textarea\n              value={this.text}\n              onChange={(event) => {\n                this.text = event.target.value;\n              }}\n              required\n              style={{width: '100%', height: '80px'}}\n            />\n          </div>\n\n          <p>\n            <button type=\"submit\" disabled={isSubmitting}>\n              Submit\n            </button>\n          </p>\n\n          {submitError && (\n            <p style={{color: 'red'}}>Sorry, an error occurred while submitting your message.</p>\n          )}\n        </form>\n      );\n    }\n  }\n\n  class Guestbook extends Routable(Component) {\n    @provide() static Message = Message;\n\n    @attribute('Message[]') static existingMessages = [];\n\n    @view() static Root() {\n      const [router, isReady] = useBrowserRouter(this);\n\n      if (!isReady) {\n        return null;\n      }\n\n      const content = router.callCurrentRoute({fallback: () => 'Sorry, there is nothing here.'});\n\n      return (\n        <div style={{maxWidth: '700px', margin: '40px auto'}}>\n          <h1>Guestbook</h1>\n          {content}\n        </div>\n      );\n    }\n\n    @route('/') @view() static Home() {\n      return (\n        <div>\n          <this.MessageList />\n          <this.MessageCreator />\n        </div>\n      );\n    }\n\n    @view() static MessageList() {\n      const {Message} = this;\n\n      const [isLoading, loadingError] = useAsyncCall(async () => {\n        this.existingMessages = await Message.find(\n          {},\n          {text: true, createdAt: true},\n          {sort: {createdAt: 'desc'}, limit: 30}\n        );\n      });\n\n      if (isLoading) {\n        return null;\n      }\n\n      if (loadingError) {\n        return (\n          <p style={{color: 'red'}}>\n            Sorry, an error occurred while loading the guestbook’s messages.\n          </p>\n        );\n      }\n\n      return (\n        <div>\n          <h2>All Messages</h2>\n          {this.existingMessages.length > 0 ? (\n            this.existingMessages.map((message) => (\n              <div key={message.id} style={{marginTop: '15px'}}>\n                <message.Viewer />\n                <div style={{marginTop: '5px'}}>\n                  <this.MessageEditor.Link params={message}>Edit</this.MessageEditor.Link>\n                </div>\n              </div>\n            ))\n          ) : (\n            <p>No messages yet.</p>\n          )}\n        </div>\n      );\n    }\n\n    @view() static MessageCreator() {\n      const {Message} = this;\n\n      const [createdMessage, resetCreatedMessage] = useRecomputableMemo(() => new Message());\n\n      const saveMessage = useCallback(async () => {\n        await createdMessage.save();\n        this.existingMessages = [createdMessage, ...this.existingMessages];\n        resetCreatedMessage();\n      }, [createdMessage]);\n\n      return (\n        <div>\n          <h2>Add a Message</h2>\n          <createdMessage.Form onSubmit={saveMessage} />\n        </div>\n      );\n    }\n\n    @route('/messages/:id') @view() static MessageEditor({id}) {\n      const {Message} = this;\n\n      const [\n        {existingMessage, editedMessage} = {},\n        isLoading,\n        loadingError\n      ] = useAsyncMemo(async () => {\n        const existingMessage = await Message.get(id, {text: true});\n        const editedMessage = existingMessage.fork();\n        return {existingMessage, editedMessage};\n      }, [id]);\n\n      const saveMessage = useCallback(async () => {\n        await editedMessage.save();\n        existingMessage.merge(editedMessage);\n        this.Home.navigate();\n      }, [existingMessage, editedMessage]);\n\n      if (isLoading) {\n        return null;\n      }\n\n      if (loadingError) {\n        return (\n          <p style={{color: 'red'}}>\n            Sorry, an error occurred while loading a guestbook’s message.\n          </p>\n        );\n      }\n\n      return (\n        <div>\n          <h2>Edit a Message</h2>\n          <editedMessage.Form onSubmit={saveMessage} />\n        </div>\n      );\n    }\n  }\n\n  ReactDOM.render(<Guestbook.Root />, document.getElementById('root'));\n}\n\nmain().catch((error) => console.error(error));\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-routes-js/src/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Guestbook</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta http-equiv=\"x-ua-compatible\" content=\"ie=edge\" />\n    <%= htmlWebpackPlugin.tags.headTags %>\n  </head>\n  <body>\n    <noscript><p>Sorry, this site requires JavaScript to be enabled.</p></noscript>\n    <div id=\"root\"></div>\n    <%= htmlWebpackPlugin.tags.bodyTags %>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-routes-js/webpack.config.js",
    "content": "const webpack = require('webpack');\nconst HtmlWebPackPlugin = require('html-webpack-plugin');\nconst path = require('path');\n\nmodule.exports = (env, argv) => {\n  return {\n    // The entry point of the app is './src/frontend.js'\n    entry: './src/frontend.js',\n    output: {\n      // Specify '/' as the base path for all the assets\n      // This is required for a single-page application\n      publicPath: '/'\n    },\n    module: {\n      rules: [\n        {\n          // Use 'babel-loader' to compile the JS files\n          test: /\\.js$/,\n          include: path.join(__dirname, 'src'),\n          loader: 'babel-loader'\n        }\n      ]\n    },\n    plugins: [\n      // Use 'html-webpack-plugin' to generate the 'index.html' file\n      // from the './src/index.html' template\n      new HtmlWebPackPlugin({\n        template: './src/index.html',\n        inject: false\n      })\n    ],\n    // Generate source maps to make debugging easier\n    devtool: 'eval-cheap-module-source-map',\n    devServer: {\n      // Fallback to 'index.html' in case of 404 responses\n      // This is required for a single-page application\n      historyApiFallback: true\n    }\n  };\n};\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-routes-ts/README.md",
    "content": "# Guestbook Web With Routes (TS)\n\nA simple web app showing how to build a web frontend using routes with Layr.\n\nSee the [corresponding guide](https://layrjs.com/docs/v1/introduction/routing?language=ts).\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-routes-ts/package.json",
    "content": "{\n  \"name\": \"guestbook-web-with-routes\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A simple web app showing how to build a web frontend using routes with Layr\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@layr/component\": \"^1.0.0\",\n    \"@layr/component-http-client\": \"^1.0.0\",\n    \"@layr/component-http-server\": \"^1.0.0\",\n    \"@layr/memory-store\": \"^1.0.0\",\n    \"@layr/react-integration\": \"^1.0.0\",\n    \"@layr/routable\": \"^1.0.0\",\n    \"@layr/storable\": \"^1.0.0\",\n    \"react\": \"^16.13.1\",\n    \"react-dom\": \"^16.13.1\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^16.9.36\",\n    \"@types/react-dom\": \"^16.9.8\",\n    \"html-webpack-plugin\": \"^4.3.0\",\n    \"ts-loader\": \"^7.0.5\",\n    \"ts-node\": \"^8.10.2\",\n    \"typescript\": \"^3.9.7\",\n    \"webpack\": \"^4.44.1\",\n    \"webpack-cli\": \"^3.3.12\",\n    \"webpack-dev-server\": \"^3.11.0\"\n  }\n}\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-routes-ts/src/backend.ts",
    "content": "import {Component, expose, validators} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\nimport {MemoryStore} from '@layr/memory-store';\nimport {ComponentHTTPServer} from '@layr/component-http-server';\n\nconst {notEmpty, maxLength} = validators;\n\n@expose({\n  find: {call: true},\n  prototype: {\n    load: {call: true},\n    save: {call: true}\n  }\n})\nexport class Message extends Storable(Component) {\n  @expose({get: true, set: true}) @primaryIdentifier() id!: string;\n\n  @expose({get: true, set: true})\n  @attribute('string', {validators: [notEmpty(), maxLength(300)]})\n  text = '';\n\n  @expose({get: true}) @attribute('Date') createdAt = new Date();\n}\n\nconst store = new MemoryStore();\n\nstore.registerStorable(Message);\n\nconst server = new ComponentHTTPServer(Message, {port: 3210});\n\nserver.start();\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-routes-ts/src/frontend.tsx",
    "content": "import React, {useCallback} from 'react';\nimport ReactDOM from 'react-dom';\nimport {Component, attribute, provide} from '@layr/component';\nimport {Storable} from '@layr/storable';\nimport {Routable, route} from '@layr/routable';\nimport {ComponentHTTPClient} from '@layr/component-http-client';\nimport {\n  view,\n  useBrowserRouter,\n  useAsyncCall,\n  useAsyncCallback,\n  useAsyncMemo,\n  useRecomputableMemo\n} from '@layr/react-integration';\n\nimport type {Message as MessageType} from './backend';\n\nasync function main() {\n  const client = new ComponentHTTPClient('http://localhost:3210', {\n    mixins: [Storable]\n  });\n\n  const BackendMessage = (await client.getComponent()) as typeof MessageType;\n\n  class Message extends BackendMessage {\n    @view() Viewer() {\n      return (\n        <div>\n          <small>{this.createdAt.toLocaleString()}</small>\n          <br />\n          <strong>{this.text}</strong>\n        </div>\n      );\n    }\n\n    @view() Form({onSubmit}: {onSubmit: () => Promise<void>}) {\n      const [handleSubmit, isSubmitting, submitError] = useAsyncCallback(async (event) => {\n        event.preventDefault();\n        await onSubmit();\n      });\n\n      return (\n        <form onSubmit={handleSubmit}>\n          <div>\n            <textarea\n              value={this.text}\n              onChange={(event) => {\n                this.text = event.target.value;\n              }}\n              required\n              style={{width: '100%', height: '80px'}}\n            />\n          </div>\n\n          <p>\n            <button type=\"submit\" disabled={isSubmitting}>\n              Submit\n            </button>\n          </p>\n\n          {submitError && (\n            <p style={{color: 'red'}}>Sorry, an error occurred while submitting your message.</p>\n          )}\n        </form>\n      );\n    }\n  }\n\n  class Guestbook extends Routable(Component) {\n    @provide() static Message = Message;\n\n    @attribute('Message[]') static existingMessages: Message[] = [];\n\n    @view() static Root() {\n      const [router, isReady] = useBrowserRouter(this);\n\n      if (!isReady) {\n        return null;\n      }\n\n      const content = router.callCurrentRoute({fallback: () => 'Sorry, there is nothing here.'});\n\n      return (\n        <div style={{maxWidth: '700px', margin: '40px auto'}}>\n          <h1>Guestbook</h1>\n          {content}\n        </div>\n      );\n    }\n\n    @route('/') @view() static Home() {\n      return (\n        <div>\n          <this.MessageList />\n          <this.MessageCreator />\n        </div>\n      );\n    }\n\n    @view() static MessageList() {\n      const {Message} = this;\n\n      const [isLoading, loadingError] = useAsyncCall(async () => {\n        this.existingMessages = await Message.find(\n          {},\n          {text: true, createdAt: true},\n          {sort: {createdAt: 'desc'}, limit: 30}\n        );\n      });\n\n      if (isLoading) {\n        return null;\n      }\n\n      if (loadingError) {\n        return (\n          <p style={{color: 'red'}}>\n            Sorry, an error occurred while loading the guestbook’s messages.\n          </p>\n        );\n      }\n\n      return (\n        <div>\n          <h2>All Messages</h2>\n          {this.existingMessages.length > 0 ? (\n            this.existingMessages.map((message) => (\n              <div key={message.id} style={{marginTop: '15px'}}>\n                <message.Viewer />\n                <div style={{marginTop: '5px'}}>\n                  <this.MessageEditor.Link params={message}>Edit</this.MessageEditor.Link>\n                </div>\n              </div>\n            ))\n          ) : (\n            <p>No messages yet.</p>\n          )}\n        </div>\n      );\n    }\n\n    @view() static MessageCreator() {\n      const {Message} = this;\n\n      const [createdMessage, resetCreatedMessage] = useRecomputableMemo(() => new Message());\n\n      const saveMessage = useCallback(async () => {\n        await createdMessage.save();\n        this.existingMessages = [createdMessage, ...this.existingMessages];\n        resetCreatedMessage();\n      }, [createdMessage]);\n\n      return (\n        <div>\n          <h2>Add a Message</h2>\n          <createdMessage.Form onSubmit={saveMessage} />\n        </div>\n      );\n    }\n\n    @route('/messages/:id') @view() static MessageEditor({id}: {id: string}) {\n      const {Message} = this;\n\n      const [{existingMessage, editedMessage} = {} as const, isLoading] = useAsyncMemo(async () => {\n        const existingMessage = await Message.get(id, {text: true});\n        const editedMessage = existingMessage.fork();\n        return {existingMessage, editedMessage};\n      }, [id]);\n\n      const saveMessage = useCallback(async () => {\n        await editedMessage!.save();\n        existingMessage!.merge(editedMessage!);\n        this.Home.navigate();\n      }, [existingMessage, editedMessage]);\n\n      if (isLoading) {\n        return null;\n      }\n\n      if (editedMessage === undefined) {\n        return (\n          <p style={{color: 'red'}}>\n            Sorry, an error occurred while loading a guestbook’s message.\n          </p>\n        );\n      }\n\n      return (\n        <div>\n          <h2>Edit a Message</h2>\n          <editedMessage.Form onSubmit={saveMessage} />\n        </div>\n      );\n    }\n  }\n\n  ReactDOM.render(<Guestbook.Root />, document.getElementById('root'));\n}\n\nmain().catch((error) => console.error(error));\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-routes-ts/src/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Guestbook</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta http-equiv=\"x-ua-compatible\" content=\"ie=edge\" />\n    <%= htmlWebpackPlugin.tags.headTags %>\n  </head>\n  <body>\n    <noscript><p>Sorry, this site requires JavaScript to be enabled.</p></noscript>\n    <div id=\"root\"></div>\n    <%= htmlWebpackPlugin.tags.bodyTags %>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-routes-ts/tsconfig.json",
    "content": "{\n  \"include\": [\"src/**/*\"],\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"module\": \"CommonJS\",\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"jsx\": \"react\",\n    \"sourceMap\": true,\n    \"strict\": true,\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"experimentalDecorators\": true,\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "examples/v1/guestbook-web-with-routes-ts/webpack.config.js",
    "content": "const webpack = require('webpack');\nconst HtmlWebPackPlugin = require('html-webpack-plugin');\nconst path = require('path');\n\nmodule.exports = (env, argv) => {\n  return {\n    // The entry point of the app is './src/frontend.tsx'\n    entry: './src/frontend.tsx',\n    output: {\n      // Specify '/' as the base path for all the assets\n      // This is required for a single-page application\n      publicPath: '/'\n    },\n    module: {\n      rules: [\n        {\n          // Use 'ts-loader' to compile the TS files\n          test: /\\.tsx?$/,\n          include: path.join(__dirname, 'src'),\n          loader: 'ts-loader'\n        }\n      ]\n    },\n    plugins: [\n      // Use 'html-webpack-plugin' to generate the 'index.html' file\n      // from the './src/index.html' template\n      new HtmlWebPackPlugin({\n        template: './src/index.html',\n        inject: false\n      })\n    ],\n    // Generate source maps to make debugging easier\n    devtool: 'eval-cheap-module-source-map',\n    devServer: {\n      // Fallback to 'index.html' in case of 404 responses\n      // This is required for a single-page application\n      historyApiFallback: true\n    }\n  };\n};\n"
  },
  {
    "path": "examples/v1/hello-world-js/README.md",
    "content": "# Hello, World! (JS)\n\nAn \"Hello, World!\" program to introduce the core concepts of Layr.\n\nSee the [corresponding guide](https://layrjs.com/docs/v1/introduction/hello-world?language=js).\n"
  },
  {
    "path": "examples/v1/hello-world-js/babel.config.json",
    "content": "{\n  \"presets\": [[\"@babel/preset-env\", {\"targets\": {\"node\": \"10\"}}]],\n  \"plugins\": [\n    [\"@babel/plugin-proposal-decorators\", {\"legacy\": true}],\n    [\"@babel/plugin-proposal-class-properties\"]\n  ]\n}\n"
  },
  {
    "path": "examples/v1/hello-world-js/jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"module\": \"CommonJS\",\n    \"checkJs\": false,\n    \"experimentalDecorators\": true\n  }\n}\n"
  },
  {
    "path": "examples/v1/hello-world-js/package.json",
    "content": "{\n  \"name\": \"hello-world-js\",\n  \"version\": \"1.0.0\",\n  \"description\": \"An \\\"Hello, World!\\\" program to introduce the core concepts of Layr.\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@layr/component\": \"^1.0.0\",\n    \"@layr/component-http-client\": \"^1.0.0\",\n    \"@layr/component-http-server\": \"^1.0.0\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.11.1\",\n    \"@babel/node\": \"^7.10.5\",\n    \"@babel/plugin-proposal-class-properties\": \"^7.10.4\",\n    \"@babel/plugin-proposal-decorators\": \"^7.10.5\",\n    \"@babel/preset-env\": \"^7.11.0\"\n  }\n}\n"
  },
  {
    "path": "examples/v1/hello-world-js/src/backend.js",
    "content": "import {Component, attribute, method, expose} from '@layr/component';\nimport {ComponentHTTPServer} from '@layr/component-http-server';\n\nexport class Greeter extends Component {\n  @expose({set: true}) @attribute('string') name = 'World';\n\n  @expose({call: true}) @method() async hello() {\n    return `Hello, ${this.name}!`;\n  }\n}\n\nconst server = new ComponentHTTPServer(Greeter, {port: 3210});\n\nserver.start();\n"
  },
  {
    "path": "examples/v1/hello-world-js/src/frontend.js",
    "content": "import {ComponentHTTPClient} from '@layr/component-http-client';\n\n(async () => {\n  const client = new ComponentHTTPClient('http://localhost:3210');\n\n  const BackendGreeter = await client.getComponent();\n\n  class Greeter extends BackendGreeter {\n    async hello() {\n      return (await super.hello()).toUpperCase();\n    }\n  }\n\n  const greeter = new Greeter({name: 'Steve'});\n\n  console.log(await greeter.hello());\n})();\n"
  },
  {
    "path": "examples/v1/hello-world-ts/README.md",
    "content": "# Hello, World! (TS)\n\nAn \"Hello, World!\" program to introduce the core concepts of Layr.\n\nSee the [corresponding guide](https://layrjs.com/docs/v1/introduction/hello-world?language=ts).\n"
  },
  {
    "path": "examples/v1/hello-world-ts/package.json",
    "content": "{\n  \"name\": \"hello-world\",\n  \"version\": \"1.0.0\",\n  \"description\": \"An \\\"Hello, World!\\\" program to introduce the core concepts of Layr.\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@layr/component\": \"^1.0.0\",\n    \"@layr/component-http-client\": \"^1.0.0\",\n    \"@layr/component-http-server\": \"^1.0.0\"\n  },\n  \"devDependencies\": {\n    \"ts-node\": \"^8.10.2\",\n    \"typescript\": \"^3.9.7\"\n  }\n}\n"
  },
  {
    "path": "examples/v1/hello-world-ts/src/backend.ts",
    "content": "import {Component, attribute, method, expose} from '@layr/component';\nimport {ComponentHTTPServer} from '@layr/component-http-server';\n\nexport class Greeter extends Component {\n  @expose({set: true}) @attribute('string') name = 'World';\n\n  @expose({call: true}) @method() async hello() {\n    return `Hello, ${this.name}!`;\n  }\n}\n\nconst server = new ComponentHTTPServer(Greeter, {port: 3210});\n\nserver.start();\n"
  },
  {
    "path": "examples/v1/hello-world-ts/src/frontend.ts",
    "content": "import {ComponentHTTPClient} from '@layr/component-http-client';\n\nimport type {Greeter as GreeterType} from './backend';\n\n(async () => {\n  const client = new ComponentHTTPClient('http://localhost:3210');\n\n  const BackendGreeter = (await client.getComponent()) as typeof GreeterType;\n\n  class Greeter extends BackendGreeter {\n    async hello() {\n      return (await super.hello()).toUpperCase();\n    }\n  }\n\n  const greeter = new Greeter({name: 'Steve'});\n\n  console.log(await greeter.hello());\n})();\n"
  },
  {
    "path": "examples/v1/hello-world-ts/tsconfig.json",
    "content": "{\n  \"include\": [\"src/**/*\"],\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"module\": \"CommonJS\",\n    \"lib\": [\"ESNext\"],\n    \"sourceMap\": true,\n    \"strict\": true,\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"experimentalDecorators\": true,\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "examples/v2/hello-world-js/.editorconfig",
    "content": "# editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.md]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": "examples/v2/hello-world-js/.gitignore",
    "content": ".DS_Store\n*.log\nnode_modules\ndist\nbuild\n_private\n*.private.*\n"
  },
  {
    "path": "examples/v2/hello-world-js/.prettierignore",
    "content": "node_modules\ndist\nbuild\n"
  },
  {
    "path": "examples/v2/hello-world-js/.vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"esbenp.prettier-vscode\",\n    \"editorconfig.editorconfig\"\n  ]\n}\n"
  },
  {
    "path": "examples/v2/hello-world-js/.vscode/settings.json",
    "content": "{\n  \"editor.formatOnSave\": true,\n  \"editor.tabSize\": 2,\n  \"editor.wordWrap\": \"bounded\",\n  \"editor.wordWrapColumn\": 100,\n  \"[javascript]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[javascriptreact]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[json]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[typescript]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[typescriptreact]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"prettier.requireConfig\": true,\n  \"prettier.useEditorConfig\": true,\n  \"eslint.enable\": false\n}\n"
  },
  {
    "path": "examples/v2/hello-world-js/README.md",
    "content": "# hello-world-js\n\n## Prerequisites\n\n- Make sure you have [Node.js](https://nodejs.org/) v16 or newer installed.\n- Make sure you have [Boostr](https://boostr.dev/) v2 installed. Boostr is used to manage the development environment.\n- If you want to deploy your app to AWS, make sure you have [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) installed and some [AWS credentials](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html) adequately set up.\n\n## Installation\n\nInstall all the npm dependencies with the following command:\n\n```sh\nboostr install\n```\n\n## Configuration\n\nIn the `backend` and `database` directories, duplicate the `boostr.config.private-example.mjs` files, name them `boostr.config.private.mjs`, and modify them to set all the required private environment variables.\n\n## Development\n\n### Migrating the database\n\nMigrate the development database with the following command:\n\n```sh\nboostr database migrate\n```\n\n### Starting the development environment\n\nStart the development environment with the following command:\n\n```sh\nboostr start\n```\n\nThe web app should be available at http://localhost:16189.\n\n## Staging\n\n### Migrating the database\n\nMigrate the staging database with the following command:\n\n```sh\nboostr database migrate --staging\n```\n\n### Deploying the app\n\nDeploy the app to your staging environment with the following command:\n\n```sh\nboostr deploy --staging\n```\n\nThe web app should be available at https://staging.example.com/.\n\n## Production\n\n### Migrating the database\n\nMigrate the production database with the following command:\n\n```sh\nboostr database migrate --production\n```\n\n### Deploying the app\n\nDeploy the app to production with the following command:\n\n```sh\nboostr deploy --production\n```\n\nThe web app should be available at https://example.com/.\n"
  },
  {
    "path": "examples/v2/hello-world-js/backend/boostr.config.mjs",
    "content": "export default ({services}) => ({\n  type: 'backend',\n\n  dependsOn: 'database',\n\n  environment: {\n    FRONTEND_URL: services.frontend.url,\n    BACKEND_URL: services.backend.url,\n    DATABASE_URL: services.database.url\n  },\n\n  rootComponent: './src/index.js',\n\n  stages: {\n    development: {\n      url: 'http://localhost:16190/',\n      platform: 'local'\n    },\n    staging: {\n      url: 'https://staging.backend.example.com/',\n      platform: 'aws',\n      aws: {\n        region: 'us-east-1',\n        lambda: {\n          memorySize: 1024\n        }\n      }\n    },\n    production: {\n      url: 'https://backend.example.com/',\n      platform: 'aws',\n      aws: {\n        region: 'us-east-1',\n        lambda: {\n          memorySize: 1024\n        }\n      }\n    }\n  }\n});\n"
  },
  {
    "path": "examples/v2/hello-world-js/backend/boostr.config.private-example.mjs",
    "content": "/**\n * This is an example of a private Boostr configuration file allowing you to\n * override the `boostr.config.mjs` public configuration.\n *\n * Duplicate this file and name it `boostr.config.private.mjs` to activate it\n * in your local development environment.\n */\n\nexport default () => ({\n  stages: {\n    development: {\n      environment: {\n        JWT_SECRET: '********'\n      }\n    },\n    staging: {\n      environment: {\n        JWT_SECRET: '********'\n      }\n    },\n    production: {\n      environment: {\n        JWT_SECRET: '********'\n      }\n    }\n  }\n});\n"
  },
  {
    "path": "examples/v2/hello-world-js/backend/jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"experimentalDecorators\": true\n  }\n}\n"
  },
  {
    "path": "examples/v2/hello-world-js/backend/package.json",
    "content": "{\n  \"name\": \"hello-world-js-backend\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@layr/component\": \"^2.0.0\",\n    \"@layr/mongodb-store\": \"^2.0.0\",\n    \"@layr/storable\": \"^2.0.0\"\n  }\n}\n"
  },
  {
    "path": "examples/v2/hello-world-js/backend/src/components/application.js",
    "content": "import {Component, method, expose} from '@layr/component';\n\nexport class Application extends Component {\n  @expose({call: true}) @method() static async getHelloWorld() {\n    const translations = ['Hello, World!', 'Bonjour le monde !', 'こんにちは世界！'];\n\n    const translation = translations[Math.round(Math.random() * (translations.length - 1))];\n\n    return translation;\n  }\n}\n"
  },
  {
    "path": "examples/v2/hello-world-js/backend/src/index.js",
    "content": "import {MongoDBStore} from '@layr/mongodb-store';\n\nimport {Application} from './components/application';\n\nexport default () => {\n  const store = new MongoDBStore(process.env.DATABASE_URL);\n\n  store.registerRootComponent(Application);\n\n  return Application;\n};\n"
  },
  {
    "path": "examples/v2/hello-world-js/boostr.config.mjs",
    "content": "export default () => ({\n  type: 'application',\n\n  services: {\n    frontend: './frontend',\n    backend: './backend',\n    database: './database'\n  },\n\n  environment: {\n    APPLICATION_NAME: 'Layr App',\n    APPLICATION_DESCRIPTION: 'A Layr app managed by Boostr'\n  },\n\n  stages: {\n    staging: {\n      environment: {\n        NODE_ENV: 'production'\n      }\n    },\n    production: {\n      environment: {\n        NODE_ENV: 'production'\n      }\n    }\n  }\n});\n"
  },
  {
    "path": "examples/v2/hello-world-js/database/.gitignore",
    "content": "/data\n"
  },
  {
    "path": "examples/v2/hello-world-js/database/boostr.config.mjs",
    "content": "export default () => ({\n  type: 'database',\n\n  stages: {\n    development: {\n      url: 'mongodb://localhost:16191/dev',\n      platform: 'local'\n    }\n  }\n});\n"
  },
  {
    "path": "examples/v2/hello-world-js/database/boostr.config.private-example.mjs",
    "content": "/**\n * This is an example of a private Boostr configuration file allowing you to\n * override the `boostr.config.mjs` public configuration.\n *\n * Duplicate this file and name it `boostr.config.private.mjs` to activate it\n * in your local development environment.\n */\n\nexport default () => ({\n  stages: {\n    staging: {\n      url: 'mongodb+srv://user:pass@clusterNane.mongodb.net/exampleStaging?retryWrites=true&w=majority'\n    },\n    production: {\n      url: 'mongodb+srv://user:pass@clusterNane.mongodb.net/exampleProduction?retryWrites=true&w=majority'\n    }\n  }\n});\n"
  },
  {
    "path": "examples/v2/hello-world-js/frontend/boostr.config.mjs",
    "content": "export default ({services}) => ({\n  type: 'web-frontend',\n\n  dependsOn: 'backend',\n\n  environment: {\n    FRONTEND_URL: services.frontend.url,\n    BACKEND_URL: services.backend.url\n  },\n\n  rootComponent: './src/index.js',\n\n  html: {\n    language: 'en',\n    head: {\n      title: services.frontend.environment.APPLICATION_NAME,\n      metas: [\n        {name: 'description', content: services.frontend.environment.APPLICATION_DESCRIPTION},\n        {charset: 'utf-8'},\n        {name: 'viewport', content: 'width=device-width, initial-scale=1'},\n        {'http-equiv': 'x-ua-compatible', 'content': 'ie=edge'}\n      ],\n      links: [{rel: 'icon', href: '/boostr-favicon-3NjLR7w1Mu8UAIqq05vVG3.immutable.png'}]\n    }\n  },\n\n  stages: {\n    development: {\n      url: 'http://localhost:16189/',\n      platform: 'local'\n    },\n    staging: {\n      url: 'https://staging.example.com/',\n      platform: 'aws',\n      aws: {\n        region: 'us-east-1',\n        cloudFront: {\n          priceClass: 'PriceClass_100'\n        }\n      }\n    },\n    production: {\n      url: 'https://example.com/',\n      platform: 'aws',\n      aws: {\n        region: 'us-east-1',\n        cloudFront: {\n          priceClass: 'PriceClass_100'\n        }\n      }\n    }\n  }\n});\n"
  },
  {
    "path": "examples/v2/hello-world-js/frontend/jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"experimentalDecorators\": true\n  }\n}\n"
  },
  {
    "path": "examples/v2/hello-world-js/frontend/package.json",
    "content": "{\n  \"name\": \"hello-world-js-frontend\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@layr/component\": \"^2.0.0\",\n    \"@layr/component-http-client\": \"^2.0.0\",\n    \"@layr/react-integration\": \"^2.0.0\",\n    \"@layr/routable\": \"^2.0.0\",\n    \"@layr/storable\": \"^2.0.0\",\n    \"react\": \"^17.0.0\",\n    \"react-dom\": \"^17.0.0\"\n  }\n}\n"
  },
  {
    "path": "examples/v2/hello-world-js/frontend/src/components/application.jsx",
    "content": "import {Routable} from '@layr/routable';\nimport React from 'react';\nimport {layout, page, useData} from '@layr/react-integration';\n\nexport const extendApplication = (Base) => {\n  class Application extends Routable(Base) {\n    @layout('/') static MainLayout({children}) {\n      return (\n        <>\n          <this.MainPage.Link>\n            <h1>{process.env.APPLICATION_NAME}</h1>\n          </this.MainPage.Link>\n          {children()}\n        </>\n      );\n    }\n\n    @page('[/]') static MainPage() {\n      return (\n        <p>\n          <this.HelloWorldPage.Link>See the \"Hello, World!\" page</this.HelloWorldPage.Link>\n        </p>\n      );\n    }\n\n    @page('[/]hello-world') static HelloWorldPage() {\n      return useData(\n        async () => await this.getHelloWorld(),\n\n        (helloWorld) => <p>{helloWorld}</p>\n      );\n    }\n\n    @page('[/]*') static NotFoundPage() {\n      return (\n        <>\n          <h2>Page not found</h2>\n          <p>Sorry, there is nothing here.</p>\n        </>\n      );\n    }\n  }\n\n  return Application;\n};\n"
  },
  {
    "path": "examples/v2/hello-world-js/frontend/src/index.js",
    "content": "import {ComponentHTTPClient} from '@layr/component-http-client';\nimport {Storable} from '@layr/storable';\n\nimport {extendApplication} from './components/application';\n\nexport default async () => {\n  const client = new ComponentHTTPClient(process.env.BACKEND_URL, {\n    mixins: [Storable],\n    async retryFailedRequests() {\n      return confirm('Sorry, a network error occurred. Would you like to retry?');\n    }\n  });\n\n  const BackendApplicationProxy = await client.getComponent();\n\n  const Application = extendApplication(BackendApplicationProxy);\n\n  return Application;\n};\n"
  },
  {
    "path": "examples/v2/hello-world-js/package.json",
    "content": "{\n  \"name\": \"hello-world-js\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"prettier\": \"@boostr/prettierrc\",\n  \"devDependencies\": {\n    \"@boostr/prettierrc\": \"^1.0.0\",\n    \"boostr\": \"^2.0.0\",\n    \"prettier\": \"^2.0.0\"\n  }\n}\n"
  },
  {
    "path": "examples/v2/hello-world-ts/.editorconfig",
    "content": "# editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.md]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": "examples/v2/hello-world-ts/.gitignore",
    "content": ".DS_Store\n*.log\nnode_modules\ndist\nbuild\n_private\n*.private.*\n"
  },
  {
    "path": "examples/v2/hello-world-ts/.prettierignore",
    "content": "node_modules\ndist\nbuild\n"
  },
  {
    "path": "examples/v2/hello-world-ts/.vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"esbenp.prettier-vscode\",\n    \"editorconfig.editorconfig\"\n  ]\n}\n"
  },
  {
    "path": "examples/v2/hello-world-ts/.vscode/settings.json",
    "content": "{\n  \"editor.formatOnSave\": true,\n  \"editor.tabSize\": 2,\n  \"editor.wordWrap\": \"bounded\",\n  \"editor.wordWrapColumn\": 100,\n  \"[javascript]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[javascriptreact]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[json]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[typescript]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[typescriptreact]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"prettier.requireConfig\": true,\n  \"prettier.useEditorConfig\": true,\n  \"eslint.enable\": false\n}\n"
  },
  {
    "path": "examples/v2/hello-world-ts/README.md",
    "content": "# hello-world-ts\n\n## Prerequisites\n\n- Make sure you have [Node.js](https://nodejs.org/) v16 or newer installed.\n- Make sure you have [Boostr](https://boostr.dev/) v2 installed. Boostr is used to manage the development environment.\n- If you want to deploy your app to AWS, make sure you have [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) installed and some [AWS credentials](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html) adequately set up.\n\n## Installation\n\nInstall all the npm dependencies with the following command:\n\n```sh\nboostr install\n```\n\n## Configuration\n\nIn the `backend` and `database` directories, duplicate the `boostr.config.private-example.mjs` files, name them `boostr.config.private.mjs`, and modify them to set all the required private environment variables.\n\n## Development\n\n### Migrating the database\n\nMigrate the development database with the following command:\n\n```sh\nboostr database migrate\n```\n\n### Starting the development environment\n\nStart the development environment with the following command:\n\n```sh\nboostr start\n```\n\nThe web app should be available at http://localhost:14951.\n\n## Staging\n\n### Migrating the database\n\nMigrate the staging database with the following command:\n\n```sh\nboostr database migrate --staging\n```\n\n### Deploying the app\n\nDeploy the app to your staging environment with the following command:\n\n```sh\nboostr deploy --staging\n```\n\nThe web app should be available at https://staging.example.com/.\n\n## Production\n\n### Migrating the database\n\nMigrate the production database with the following command:\n\n```sh\nboostr database migrate --production\n```\n\n### Deploying the app\n\nDeploy the app to production with the following command:\n\n```sh\nboostr deploy --production\n```\n\nThe web app should be available at https://example.com/.\n"
  },
  {
    "path": "examples/v2/hello-world-ts/backend/boostr.config.mjs",
    "content": "export default ({services}) => ({\n  type: 'backend',\n\n  dependsOn: 'database',\n\n  environment: {\n    FRONTEND_URL: services.frontend.url,\n    BACKEND_URL: services.backend.url,\n    DATABASE_URL: services.database.url\n  },\n\n  rootComponent: './src/index.ts',\n\n  stages: {\n    development: {\n      url: 'http://localhost:14952/',\n      platform: 'local'\n    },\n    staging: {\n      url: 'https://staging.backend.example.com/',\n      platform: 'aws',\n      aws: {\n        region: 'us-east-1',\n        lambda: {\n          memorySize: 1024\n        }\n      }\n    },\n    production: {\n      url: 'https://backend.example.com/',\n      platform: 'aws',\n      aws: {\n        region: 'us-east-1',\n        lambda: {\n          memorySize: 1024\n        }\n      }\n    }\n  }\n});\n"
  },
  {
    "path": "examples/v2/hello-world-ts/backend/boostr.config.private-example.mjs",
    "content": "/**\n * This is an example of a private Boostr configuration file allowing you to\n * override the `boostr.config.mjs` public configuration.\n *\n * Duplicate this file and name it `boostr.config.private.mjs` to activate it\n * in your local development environment.\n */\n\nexport default () => ({\n  stages: {\n    development: {\n      environment: {\n        JWT_SECRET: '********'\n      }\n    },\n    staging: {\n      environment: {\n        JWT_SECRET: '********'\n      }\n    },\n    production: {\n      environment: {\n        JWT_SECRET: '********'\n      }\n    }\n  }\n});\n"
  },
  {
    "path": "examples/v2/hello-world-ts/backend/package.json",
    "content": "{\n  \"name\": \"hello-world-ts-backend\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@layr/component\": \"^2.0.0\",\n    \"@layr/mongodb-store\": \"^2.0.0\",\n    \"@layr/storable\": \"^2.0.0\"\n  },\n  \"devDependencies\": {\n    \"@boostr/tsconfig\": \"^1.0.0\",\n    \"@types/node\": \"^18.0.0\"\n  }\n}\n"
  },
  {
    "path": "examples/v2/hello-world-ts/backend/src/components/application.ts",
    "content": "import {Component, method, expose} from '@layr/component';\n\nexport class Application extends Component {\n  @expose({call: true}) @method() static async getHelloWorld() {\n    const translations = ['Hello, World!', 'Bonjour le monde !', 'こんにちは世界！'];\n\n    const translation = translations[Math.round(Math.random() * (translations.length - 1))];\n\n    return translation;\n  }\n}\n"
  },
  {
    "path": "examples/v2/hello-world-ts/backend/src/index.ts",
    "content": "import {MongoDBStore} from '@layr/mongodb-store';\n\nimport {Application} from './components/application';\n\nexport default () => {\n  const store = new MongoDBStore(process.env.DATABASE_URL!);\n\n  store.registerRootComponent(Application);\n\n  return Application;\n};\n"
  },
  {
    "path": "examples/v2/hello-world-ts/backend/tsconfig.json",
    "content": "{\n  \"extends\": \"@boostr/tsconfig\",\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "examples/v2/hello-world-ts/boostr.config.mjs",
    "content": "export default () => ({\n  type: 'application',\n\n  services: {\n    frontend: './frontend',\n    backend: './backend',\n    database: './database'\n  },\n\n  environment: {\n    APPLICATION_NAME: 'Layr App',\n    APPLICATION_DESCRIPTION: 'A Layr app managed by Boostr'\n  },\n\n  stages: {\n    staging: {\n      environment: {\n        NODE_ENV: 'production'\n      }\n    },\n    production: {\n      environment: {\n        NODE_ENV: 'production'\n      }\n    }\n  }\n});\n"
  },
  {
    "path": "examples/v2/hello-world-ts/database/.gitignore",
    "content": "/data\n"
  },
  {
    "path": "examples/v2/hello-world-ts/database/boostr.config.mjs",
    "content": "export default () => ({\n  type: 'database',\n\n  stages: {\n    development: {\n      url: 'mongodb://localhost:14953/dev',\n      platform: 'local'\n    }\n  }\n});\n"
  },
  {
    "path": "examples/v2/hello-world-ts/database/boostr.config.private-example.mjs",
    "content": "/**\n * This is an example of a private Boostr configuration file allowing you to\n * override the `boostr.config.mjs` public configuration.\n *\n * Duplicate this file and name it `boostr.config.private.mjs` to activate it\n * in your local development environment.\n */\n\nexport default () => ({\n  stages: {\n    staging: {\n      url: 'mongodb+srv://user:pass@clusterNane.mongodb.net/exampleStaging?retryWrites=true&w=majority'\n    },\n    production: {\n      url: 'mongodb+srv://user:pass@clusterNane.mongodb.net/exampleProduction?retryWrites=true&w=majority'\n    }\n  }\n});\n"
  },
  {
    "path": "examples/v2/hello-world-ts/frontend/boostr.config.mjs",
    "content": "export default ({services}) => ({\n  type: 'web-frontend',\n\n  dependsOn: 'backend',\n\n  environment: {\n    FRONTEND_URL: services.frontend.url,\n    BACKEND_URL: services.backend.url\n  },\n\n  rootComponent: './src/index.ts',\n\n  html: {\n    language: 'en',\n    head: {\n      title: services.frontend.environment.APPLICATION_NAME,\n      metas: [\n        {name: 'description', content: services.frontend.environment.APPLICATION_DESCRIPTION},\n        {charset: 'utf-8'},\n        {name: 'viewport', content: 'width=device-width, initial-scale=1'},\n        {'http-equiv': 'x-ua-compatible', 'content': 'ie=edge'}\n      ],\n      links: [{rel: 'icon', href: '/boostr-favicon-3NjLR7w1Mu8UAIqq05vVG3.immutable.png'}]\n    }\n  },\n\n  stages: {\n    development: {\n      url: 'http://localhost:14951/',\n      platform: 'local'\n    },\n    staging: {\n      url: 'https://staging.example.com/',\n      platform: 'aws',\n      aws: {\n        region: 'us-east-1',\n        cloudFront: {\n          priceClass: 'PriceClass_100'\n        }\n      }\n    },\n    production: {\n      url: 'https://example.com/',\n      platform: 'aws',\n      aws: {\n        region: 'us-east-1',\n        cloudFront: {\n          priceClass: 'PriceClass_100'\n        }\n      }\n    }\n  }\n});\n"
  },
  {
    "path": "examples/v2/hello-world-ts/frontend/package.json",
    "content": "{\n  \"name\": \"hello-world-ts-frontend\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@layr/component\": \"^2.0.0\",\n    \"@layr/component-http-client\": \"^2.0.0\",\n    \"@layr/react-integration\": \"^2.0.0\",\n    \"@layr/routable\": \"^2.0.0\",\n    \"@layr/storable\": \"^2.0.0\",\n    \"react\": \"^17.0.0\",\n    \"react-dom\": \"^17.0.0\"\n  },\n  \"devDependencies\": {\n    \"@boostr/tsconfig\": \"^1.0.0\",\n    \"@types/node\": \"^18.0.0\",\n    \"@types/react\": \"^17.0.0\",\n    \"@types/react-dom\": \"^17.0.0\"\n  }\n}\n"
  },
  {
    "path": "examples/v2/hello-world-ts/frontend/src/components/application.tsx",
    "content": "import {Routable} from '@layr/routable';\nimport React, {Fragment} from 'react';\nimport {layout, page, useData} from '@layr/react-integration';\n\nimport type {Application as BackendApplication} from '../../../backend/src/components/application';\n\nexport const extendApplication = (Base: typeof BackendApplication) => {\n  class Application extends Routable(Base) {\n    declare ['constructor']: typeof Application;\n\n    @layout('/') static MainLayout({children}: {children: () => any}) {\n      return (\n        <>\n          <this.MainPage.Link>\n            <h1>{process.env.APPLICATION_NAME}</h1>\n          </this.MainPage.Link>\n          {children()}\n        </>\n      );\n    }\n\n    @page('[/]') static MainPage() {\n      return (\n        <p>\n          <this.HelloWorldPage.Link>See the \"Hello, World!\" page</this.HelloWorldPage.Link>\n        </p>\n      );\n    }\n\n    @page('[/]hello-world') static HelloWorldPage() {\n      return useData(\n        async () => await this.getHelloWorld(),\n\n        (helloWorld) => <p>{helloWorld}</p>\n      );\n    }\n\n    @page('[/]*') static NotFoundPage() {\n      return (\n        <>\n          <h2>Page not found</h2>\n          <p>Sorry, there is nothing here.</p>\n        </>\n      );\n    }\n  }\n\n  return Application;\n};\n\nexport declare const Application: ReturnType<typeof extendApplication>;\n\nexport type Application = InstanceType<typeof Application>;\n"
  },
  {
    "path": "examples/v2/hello-world-ts/frontend/src/index.ts",
    "content": "import {ComponentHTTPClient} from '@layr/component-http-client';\nimport {Storable} from '@layr/storable';\n\nimport type {Application as BackendApplication} from '../../backend/src/components/application';\nimport {extendApplication} from './components/application';\n\nexport default async () => {\n  const client = new ComponentHTTPClient(process.env.BACKEND_URL!, {\n    mixins: [Storable],\n    async retryFailedRequests() {\n      return confirm('Sorry, a network error occurred. Would you like to retry?');\n    }\n  });\n\n  const BackendApplicationProxy = (await client.getComponent()) as typeof BackendApplication;\n\n  const Application = extendApplication(BackendApplicationProxy);\n\n  return Application;\n};\n"
  },
  {
    "path": "examples/v2/hello-world-ts/frontend/tsconfig.json",
    "content": "{\n  \"extends\": \"@boostr/tsconfig\",\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "examples/v2/hello-world-ts/package.json",
    "content": "{\n  \"name\": \"hello-world-ts\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"prettier\": \"@boostr/prettierrc\",\n  \"devDependencies\": {\n    \"@boostr/prettierrc\": \"^1.0.0\",\n    \"boostr\": \"^2.0.0\",\n    \"prettier\": \"^2.0.0\"\n  }\n}\n"
  },
  {
    "path": "lerna.json",
    "content": "{\n  \"version\": \"independent\",\n  \"npmClient\": \"npm\",\n  \"packages\": [\"packages/*\"]\n}\n"
  },
  {
    "path": "nx.json",
    "content": "{\n  \"tasksRunnerOptions\": {\n    \"default\": {\n      \"runner\": \"nx/tasks-runners/default\",\n      \"options\": {\n        \"cacheableOperations\": [\n          \"build\"\n        ]\n      }\n    }\n  },\n  \"targetDefaults\": {\n    \"build\": {\n      \"dependsOn\": [\n        \"^build\"\n      ],\n      \"outputs\": [\n        \"{projectRoot}/dist\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"layr\",\n  \"version\": \"2.0.1\",\n  \"private\": true,\n  \"description\": \"Dramatically simplify full‑stack development\",\n  \"keywords\": [],\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/layrjs/layr\",\n  \"scripts\": {\n    \"build:docs\": \"(cd ./docs && npm run build)\"\n  },\n  \"prettier\": \"@mvila/prettierrc\",\n  \"devDependencies\": {\n    \"@mvila/prettierrc\": \"^1.1.2\",\n    \"lerna\": \"^6.4.0\",\n    \"prettier\": \"^2.8.2\"\n  }\n}\n"
  },
  {
    "path": "packages/aws-integration/README.md",
    "content": "# @layr/aws-integration\n\nAWS integration for Layr.\n\n## Installation\n\n```\nnpm install @layr/aws-integration\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/aws-integration/package.json",
    "content": "{\n  \"name\": \"@layr/aws-integration\",\n  \"version\": \"2.0.101\",\n  \"description\": \"AWS integration for Layr\",\n  \"keywords\": [\n    \"layr\",\n    \"aws\",\n    \"integration\",\n    \"lambda\",\n    \"handler\"\n  ],\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/layrjs/layr/tree/master/packages/aws-integration\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"dist/node-cjs/index.js\",\n  \"module\": \"dist/node-esm/index.js\",\n  \"engines\": {\n    \"node\": \">=16.0.0\"\n  },\n  \"scripts\": {\n    \"build\": \"dev-tools build:ts-library\",\n    \"prepare\": \"npm run build\",\n    \"publish:package\": \"dev-tools publish:package\",\n    \"update\": \"dev-tools update:dependencies\"\n  },\n  \"dependencies\": {\n    \"@layr/component\": \"^2.0.51\",\n    \"@layr/component-server\": \"^2.0.70\",\n    \"@layr/routable\": \"^2.0.113\",\n    \"core-helpers\": \"^1.0.8\",\n    \"lodash\": \"^4.17.21\",\n    \"tslib\": \"^2.4.1\"\n  },\n  \"devDependencies\": {\n    \"@mvila/dev-tools\": \"^1.3.1\",\n    \"@mvila/tsconfig\": \"^1.0.6\",\n    \"@types/aws-lambda\": \"^8.10.109\",\n    \"@types/lodash\": \"^4.14.191\",\n    \"aws-sdk\": \"^2.1290.0\"\n  }\n}\n"
  },
  {
    "path": "packages/aws-integration/src/index.ts",
    "content": "/**\n * @module aws-integration\n *\n * Allows you to host a [component server](https://layrjs.com/docs/v2/reference/component-server) in [AWS Lambda](https://aws.amazon.com/lambda/).\n */\n\nexport * from './lambda-execution-queue-sender';\nexport * from './lambda-handler';\n"
  },
  {
    "path": "packages/aws-integration/src/lambda-execution-queue-sender.ts",
    "content": "import type AWS from 'aws-sdk';\nimport type {PlainObject} from 'core-helpers';\n\nexport function createAWSLambdaExecutionQueueSender({\n  lambdaClient,\n  functionName\n}: {\n  lambdaClient: AWS.Lambda;\n  functionName: string;\n}) {\n  return async (query: PlainObject) => {\n    await lambdaClient\n      .invoke({\n        FunctionName: functionName,\n        InvocationType: 'Event',\n        Payload: JSON.stringify({query})\n      })\n      .promise();\n  };\n}\n"
  },
  {
    "path": "packages/aws-integration/src/lambda-handler.ts",
    "content": "import type {Component} from '@layr/component';\nimport {isComponentClass} from '@layr/component';\nimport type {ComponentServer, ComponentServerOptions} from '@layr/component-server';\nimport {ensureComponentServer, isComponentServerInstance} from '@layr/component-server';\nimport type {RoutableComponent} from '@layr/routable';\nimport {callRouteByURL, isRoutableClass} from '@layr/routable';\nimport type {APIGatewayProxyEventV2, Context, APIGatewayProxyStructuredResultV2} from 'aws-lambda';\n\ntype CustomHandler = (\n  event: APIGatewayProxyEventV2,\n  context: Context\n) => Promise<APIGatewayProxyStructuredResultV2 | undefined>;\n\n/**\n * Creates an [AWS Lambda function handler](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html) for the specified [ComponentServer](https://layrjs.com/docs/v2/reference/component-server) instance.\n *\n * Alternatively, you can pass a [`Component`](https://layrjs.com/docs/v2/reference/component) class or a function (which can be asynchronous) returning a [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server) instance or a [`Component`](https://layrjs.com/docs/v2/reference/component) class. When a [`Component`](https://layrjs.com/docs/v2/reference/component) class is passed (or returned from a passed function), a [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server) instance is automatically created from the [`Component`](https://layrjs.com/docs/v2/reference/component) class.\n *\n * The created handler can be hosted in [AWS Lambda](https://aws.amazon.com/lambda/) and consumed by [AWS API Gateway](https://aws.amazon.com/api-gateway/) through an [HTTP API](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api.html).\n *\n * @param componentServer A [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server) instance, a [`Component`](https://layrjs.com/docs/v2/reference/component), or a function (which can be asynchronous) returning a [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server) instance or a [`Component`](https://layrjs.com/docs/v2/reference/component).\n *\n * @returns An [AWS Lambda function handler](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html).\n *\n * @example\n * ```\n * import {Component, attribute, expose} from '﹫layr/component';\n * import {ComponentServer} from '﹫layr/component-server';\n * import {createAWSLambdaHandler} from '@layr/aws-integration';\n *\n * class Movie extends Component {\n *   ﹫expose({get: true, set: true}) ﹫attribute('string') title = '';\n * }\n *\n * const server = new ComponentServer(Movie);\n *\n * const handler = createAWSLambdaHandler(server);\n *\n * export {handler};\n * ```\n *\n * @category Functions\n */\nexport function createAWSLambdaHandler(\n  componentServerProvider:\n    | typeof Component\n    | ComponentServer\n    | (() => Promise<typeof Component | ComponentServer>),\n  options: ComponentServerOptions & {customHandler?: CustomHandler} = {}\n) {\n  const {customHandler, ...componentServerOptions} = options;\n\n  let componentServer: ComponentServer;\n  let routableComponent: typeof RoutableComponent | undefined;\n\n  const handler = async (\n    event: APIGatewayProxyEventV2,\n    context: Context\n  ): Promise<APIGatewayProxyStructuredResultV2 | undefined> => {\n    context.callbackWaitsForEmptyEventLoop = false;\n\n    if (componentServer === undefined) {\n      if (\n        isComponentClass(componentServerProvider) ||\n        isComponentServerInstance(componentServerProvider)\n      ) {\n        componentServer = ensureComponentServer(componentServerProvider, componentServerOptions);\n      } else if (typeof componentServerProvider === 'function') {\n        componentServer = ensureComponentServer(\n          await componentServerProvider(),\n          componentServerOptions\n        );\n      } else {\n        throw new Error(\n          `Expected a component class, a componentServer, or a function returning a component class or componentServer, but received a value of type '${typeof componentServerProvider}'`\n        );\n      }\n\n      const component = componentServer.getComponent();\n      routableComponent = isRoutableClass(component) ? component : undefined;\n    }\n\n    if (\n      !(\n        event.version === '2.0' &&\n        event.rawPath !== undefined &&\n        event.requestContext !== undefined\n      )\n    ) {\n      // === Background invocation via an EventBridge rule or a Lambda asynchronous call ===\n\n      await componentServer.receive(event as any, {executionMode: 'background'});\n\n      return;\n    }\n\n    // === Foreground invocation via an API Gateway HTTP API v2 request ===\n\n    if (customHandler !== undefined) {\n      const result = await customHandler(event, context);\n\n      if (result !== undefined) {\n        return result;\n      }\n    }\n\n    const method = event.requestContext.http.method;\n\n    if (method === 'OPTIONS') {\n      return {statusCode: 200};\n    }\n\n    const path = event.rawPath;\n\n    if (path === '/') {\n      if (method === 'GET') {\n        return await receiveRequest({query: {'introspect=>': {'()': []}}});\n      }\n\n      if (method === 'POST') {\n        const body = event.body;\n\n        if (typeof body !== 'string') {\n          return {statusCode: 400, body: 'Bad Request'};\n        }\n\n        let request: any;\n\n        try {\n          request = JSON.parse(body);\n        } catch {\n          return {statusCode: 400, body: 'Bad Request'};\n        }\n\n        return await receiveRequest(request);\n      }\n\n      return {statusCode: 405, body: 'Method Not Allowed'};\n    }\n\n    if (routableComponent !== undefined) {\n      const routableComponentFork = routableComponent.fork();\n\n      await routableComponentFork.initialize();\n\n      let url = path;\n\n      if (event.rawQueryString) {\n        url += `?${event.rawQueryString}`;\n      }\n\n      const headers = event.headers;\n\n      let body: string | Buffer | undefined = event.body;\n\n      if (body !== undefined && event.isBase64Encoded) {\n        body = Buffer.from(body, 'base64');\n      }\n\n      const response: {\n        status: number;\n        headers?: Record<string, string>;\n        body?: string | Buffer;\n      } = await callRouteByURL(routableComponentFork, url, {method, headers, body});\n\n      if (typeof response?.status !== 'number') {\n        throw new Error(\n          `Unexpected response \\`${JSON.stringify(\n            response\n          )}\\` returned by a component route (a proper response should be an object of the shape \\`{status: number; headers?: Record<string, string>; body?: string | Buffer;}\\`)`\n        );\n      }\n\n      return {\n        statusCode: response.status,\n        headers: response.headers,\n        body: Buffer.isBuffer(response.body) ? response.body.toString('base64') : response.body,\n        isBase64Encoded: Buffer.isBuffer(response.body)\n      };\n    }\n\n    return {statusCode: 404, body: 'Not Found'};\n  };\n\n  const receiveRequest = async (request: any): Promise<APIGatewayProxyStructuredResultV2> => {\n    const response = await componentServer.receive(request);\n\n    return {\n      statusCode: 200,\n      headers: {'content-type': 'application/json'},\n      body: JSON.stringify(response)\n    };\n  };\n\n  return handler;\n}\n"
  },
  {
    "path": "packages/aws-integration/tsconfig.json",
    "content": "{\n  \"extends\": \"@mvila/tsconfig\",\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/browser-navigator/README.md",
    "content": "# @layr/browser-navigator\n\nProvides a navigation system for a Layr app running in a browser.\n\n## Installation\n\n```\nnpm install @layr/browser-navigator\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/browser-navigator/package.json",
    "content": "{\n  \"name\": \"@layr/browser-navigator\",\n  \"version\": \"2.0.67\",\n  \"description\": \"Provides a navigation system for a Layr app running in a browser\",\n  \"keywords\": [\n    \"layr\",\n    \"navigator\",\n    \"navigation\",\n    \"browser\"\n  ],\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/layrjs/layr/tree/master/packages/browser-navigator\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"dist/node-cjs/index.js\",\n  \"module\": \"dist/node-esm/index.js\",\n  \"engines\": {\n    \"node\": \">=16.0.0\"\n  },\n  \"scripts\": {\n    \"build\": \"dev-tools build:ts-library\",\n    \"prepare\": \"npm run build\",\n    \"publish:package\": \"dev-tools publish:package\",\n    \"update\": \"dev-tools update:dependencies\"\n  },\n  \"dependencies\": {\n    \"@layr/navigator\": \"^2.0.55\",\n    \"core-helpers\": \"^1.0.8\",\n    \"lodash\": \"^4.17.21\",\n    \"tslib\": \"^2.4.1\"\n  },\n  \"devDependencies\": {\n    \"@mvila/dev-tools\": \"^1.3.1\",\n    \"@mvila/tsconfig\": \"^1.0.6\",\n    \"@types/lodash\": \"^4.14.191\",\n    \"@types/react\": \"^16.14.34\"\n  }\n}\n"
  },
  {
    "path": "packages/browser-navigator/src/browser-navigator.ts",
    "content": "import {Navigator, NavigatorOptions, normalizeURL, stringifyURL} from '@layr/navigator';\nimport debounce from 'lodash/debounce';\n\nexport type BrowserNavigatorLinkProps = {\n  to: string;\n  className?: string;\n  activeClassName?: string;\n  style?: React.CSSProperties;\n  activeStyle?: React.CSSProperties;\n};\n\nexport type BrowserNavigatorOptions = NavigatorOptions;\n\n/**\n * *Inherits from [`Navigator`](https://layrjs.com/docs/v2/reference/navigator).*\n *\n * A [`Navigator`](https://layrjs.com/docs/v2/reference/navigator) relying on the browser's [History API](https://developer.mozilla.org/en-US/docs/Web/API/History) to determine the current [route](https://layrjs.com/docs/v2/reference/route).\n *\n * #### Usage\n *\n * If you are using [React](https://reactjs.org/), the easiest way to set up a `BrowserNavigator` in your app is to use the [`BrowserNavigatorView`](https://layrjs.com/docs/v2/reference/react-integration#browser-navigator-view-react-component) React component that is provided by the `@layr/react-integration` package.\n *\n * See an example of use in the [`BrowserNavigatorView`](https://layrjs.com/docs/v2/reference/react-integration#browser-navigator-view-react-component) React component.\n */\nexport class BrowserNavigator extends Navigator {\n  /**\n   * Creates a [`BrowserNavigator`](https://layrjs.com/docs/v2/reference/browser-navigator).\n   *\n   * @returns The [`BrowserNavigator`](https://layrjs.com/docs/v2/reference/browser-navigator) instance that was created.\n   *\n   * @category Creation\n   */\n  constructor(options: BrowserNavigatorOptions = {}) {\n    super(options);\n  }\n\n  _popStateHandler!: (event: PopStateEvent) => void;\n  _ignorePopStateHandler!: boolean;\n  _navigateHandler!: (event: Event) => void;\n  _mutationObserver!: MutationObserver;\n  _expectedHash: string | undefined;\n  _scrollToHash!: () => void;\n\n  mount() {\n    // --- Back/forward navigation ---\n\n    this._popStateHandler = () => {\n      if (!this._ignorePopStateHandler) {\n        this.callObservers();\n      }\n    };\n\n    this._ignorePopStateHandler = false;\n\n    window.addEventListener('popstate', this._popStateHandler);\n\n    this._navigateHandler = (event: Event) => {\n      this.navigate((event as CustomEvent).detail.url);\n    };\n\n    // --- 'layrNavigatorNavigate' event ---\n\n    document.body.addEventListener('layrNavigatorNavigate', this._navigateHandler);\n\n    // --- Hash navigation fix ---\n\n    this._scrollToHash = debounce(() => {\n      const element = document.getElementById(this._expectedHash!);\n\n      if (element !== null) {\n        element.scrollIntoView({block: 'start', behavior: 'smooth'});\n        this._expectedHash = undefined;\n      }\n    }, 50);\n\n    this._mutationObserver = new MutationObserver(() => {\n      if (this._expectedHash !== undefined) {\n        this._scrollToHash();\n      }\n    });\n\n    this._mutationObserver.observe(document.body, {\n      attributes: true,\n      characterData: true,\n      childList: true,\n      subtree: true\n    });\n\n    this._fixScrollPosition();\n  }\n\n  unmount() {\n    window.removeEventListener('popstate', this._popStateHandler);\n    window.removeEventListener('popstate', this._navigateHandler);\n    this._mutationObserver.disconnect();\n  }\n\n  // === Component Registration ===\n\n  /**\n   * See the methods that are inherited from the [`Navigator`](https://layrjs.com/docs/v2/reference/navigator#component-registration) class.\n   *\n   * @category Component Registration\n   */\n\n  // === Routes ===\n\n  /**\n   * See the methods that are inherited from the [`Navigator`](https://layrjs.com/docs/v2/reference/navigator#routes) class.\n   *\n   * @category Routes\n   */\n\n  // === Current Location ===\n\n  /**\n   * See the methods that are inherited from the [`Navigator`](https://layrjs.com/docs/v2/reference/navigator#current-location) class.\n   *\n   * @category Current Location\n   */\n\n  _getCurrentURL() {\n    return normalizeURL(window.location.href);\n  }\n\n  // === Navigation ===\n\n  /**\n   * See the methods that are inherited from the [`Navigator`](https://layrjs.com/docs/v2/reference/navigator#navigation) class.\n   *\n   * @category Navigation\n   */\n\n  _navigate(url: URL) {\n    window.history.pushState({index: this._getHistoryIndex() + 1}, '', stringifyURL(url));\n    this._fixScrollPosition();\n  }\n\n  _redirect(url: URL) {\n    window.history.replaceState({index: this._getHistoryIndex()}, '', stringifyURL(url));\n    this._fixScrollPosition();\n  }\n\n  _fixScrollPosition() {\n    window.scrollTo(0, 0);\n\n    const hash = this.getCurrentHash();\n\n    if (hash !== undefined) {\n      this._expectedHash = hash;\n      this._scrollToHash();\n    }\n  }\n\n  _reload(url: URL | undefined) {\n    if (url !== undefined) {\n      window.location.replace(stringifyURL(url));\n    } else {\n      window.location.reload();\n    }\n  }\n\n  _go(delta: number) {\n    return new Promise<void>((resolve) => {\n      this._ignorePopStateHandler = true;\n\n      window.addEventListener(\n        'popstate',\n        () => {\n          this._ignorePopStateHandler = false;\n          resolve();\n        },\n        {once: true}\n      );\n\n      window.history.go(delta);\n    });\n  }\n\n  _getHistoryLength() {\n    return window.history.length;\n  }\n\n  _getHistoryIndex() {\n    return (window.history.state?.index ?? 0) as number;\n  }\n\n  /**\n   * Renders a link that is managed by the navigator.\n   *\n   * This method is only available when you create your navigator by using the [`useBrowserNavigator()`](https://layrjs.com/docs/v2/reference/react-integration#use-browser-navigator-react-hook) React hook.\n   *\n   * Note that instead of using this method, you can use the handy `Link()` shortcut function that you get when you define a route with the [`@route()`](https://layrjs.com/docs/v2/reference/routable#route-decorator) decorator.\n   *\n   * @param props.to A string representing the target URL of the link.\n   * @param props.className A [`className`](https://reactjs.org/docs/dom-elements.html#classname) attribute to apply to the rendered link.\n   * @param props.activeClassName An additional [`className`](https://reactjs.org/docs/dom-elements.html#classname) attribute to apply to the rendered link when the URL of the current navigator's route is the same as the target URL of the link.\n   * @param props.style A [`style`](https://reactjs.org/docs/dom-elements.html#style) attribute to apply to the rendered link.\n   * @param props.activeStyle An additional [`style`](https://reactjs.org/docs/dom-elements.html#style) attribute to apply to the rendered link when the URL of the current navigator's route is the same as the target URL of the link.\n   *\n   * @returns An `<a>` React element.\n   *\n   * @example\n   * ```\n   * class Application extends Routable(Component) {\n   *    ﹫route('/') @view static Home() {\n   *      const navigator = this.getNavigator();\n   *      return <navigator.Link to=\"/about-us\">About Us</navigator.Link>;\n   *\n   *      // Same as above, but in a more idiomatic way:\n   *      return <this.AboutUs.Link>About Us</this.AboutUs.Link>;\n   *    }\n   *\n   *    ﹫route('/about-us') @view static AboutUs() {\n   *      return <div>Here is everything about us.<div>;\n   *    }\n   * }\n   * ```\n   *\n   * @category Link Rendering\n   */\n  Link!: (props: BrowserNavigatorLinkProps) => any;\n\n  // === Observability ===\n\n  /**\n   * See the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) class.\n   *\n   * @category Observability\n   */\n}\n"
  },
  {
    "path": "packages/browser-navigator/src/index.ts",
    "content": "export * from './browser-navigator';\nexport * from './utilities';\n"
  },
  {
    "path": "packages/browser-navigator/src/utilities.ts",
    "content": "export function isInternalURL(url: URL | string) {\n  if (!(url instanceof URL)) {\n    url = new URL(url, 'internal:/');\n  }\n\n  if (url.protocol === 'internal:') {\n    return true;\n  }\n\n  const currentURL = new URL(window.location.href);\n\n  if (\n    url.protocol === currentURL.protocol &&\n    url.username === currentURL.username &&\n    url.password === currentURL.password &&\n    url.host === currentURL.host\n  ) {\n    return true;\n  }\n\n  return false;\n}\n"
  },
  {
    "path": "packages/browser-navigator/tsconfig.json",
    "content": "{\n  \"extends\": \"@mvila/tsconfig\",\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/component/README.md",
    "content": "# @layr/component\n\nThe base class of all your components.\n\n## Installation\n\n```\nnpm install @layr/component\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/component/package.json",
    "content": "{\n  \"name\": \"@layr/component\",\n  \"version\": \"2.0.51\",\n  \"description\": \"The base class of all your components\",\n  \"keywords\": [\n    \"layr\",\n    \"component\",\n    \"base\",\n    \"class\"\n  ],\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/layrjs/layr/tree/master/packages/component\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"dist/node-cjs/index.js\",\n  \"module\": \"dist/node-esm/index.js\",\n  \"engines\": {\n    \"node\": \">=16.0.0\"\n  },\n  \"scripts\": {\n    \"build\": \"dev-tools build:ts-library\",\n    \"prepare\": \"npm run build\",\n    \"test\": \"dev-tools test:ts-library\",\n    \"publish:package\": \"dev-tools publish:package\",\n    \"update\": \"dev-tools update:dependencies\"\n  },\n  \"dependencies\": {\n    \"@layr/observable\": \"^1.0.16\",\n    \"@layr/utilities\": \"^1.0.9\",\n    \"core-helpers\": \"^1.0.8\",\n    \"cuid\": \"^2.1.8\",\n    \"lodash\": \"^4.17.21\",\n    \"possibly-async\": \"^1.0.7\",\n    \"simple-cloning\": \"^1.0.5\",\n    \"simple-forking\": \"^1.0.7\",\n    \"simple-serialization\": \"^1.0.12\",\n    \"tslib\": \"^2.4.1\"\n  },\n  \"devDependencies\": {\n    \"@mvila/dev-tools\": \"^1.3.1\",\n    \"@mvila/tsconfig\": \"^1.0.6\",\n    \"@types/jest\": \"^29.2.5\",\n    \"@types/lodash\": \"^4.14.191\",\n    \"sleep-promise\": \"^9.1.0\"\n  }\n}\n"
  },
  {
    "path": "packages/component/src/cloning.test.ts",
    "content": "import {Component} from './component';\nimport {attribute, primaryIdentifier, provide} from './decorators';\n\ndescribe('Cloning', () => {\n  test('Simple component', async () => {\n    class Movie extends Component {\n      @attribute() title?: string;\n      @attribute() tags?: string[];\n      @attribute() specs?: {duration?: number};\n    }\n\n    expect(Movie.clone()).toBe(Movie);\n\n    let movie = new Movie({title: 'Inception', tags: ['drama'], specs: {duration: 120}});\n\n    let clonedMovie = movie.clone();\n\n    expect(clonedMovie).not.toBe(movie);\n    expect(clonedMovie.getComponentType()).toBe('Movie');\n    expect(clonedMovie.isNew()).toBe(true);\n    expect(clonedMovie.title).toBe(movie.title);\n    expect(clonedMovie.tags).not.toBe(movie.tags);\n    expect(clonedMovie.tags).toEqual(movie.tags);\n    expect(clonedMovie.specs).not.toBe(movie.specs);\n    expect(clonedMovie.specs).toEqual(movie.specs);\n\n    movie = Movie.instantiate();\n    movie.title = 'Inception';\n\n    clonedMovie = movie.clone();\n\n    expect(clonedMovie).not.toBe(movie);\n    expect(clonedMovie.getComponentType()).toBe('Movie');\n    expect(clonedMovie.isNew()).toBe(false);\n    expect(clonedMovie.title).toBe(movie.title);\n    expect(clonedMovie.getAttribute('tags').isSet()).toBe(false);\n    expect(clonedMovie.getAttribute('specs').isSet()).toBe(false);\n  });\n\n  test('Referenced component', async () => {\n    class Movie extends Component {\n      @attribute() director!: Director;\n    }\n\n    class Director extends Component {\n      @attribute() name?: string;\n    }\n\n    const movie = new Movie({director: new Director({name: 'Christopher Nolan'})});\n\n    const clonedMovie = movie.clone();\n\n    expect(clonedMovie.director.getComponentType()).toBe('Director');\n    expect(clonedMovie.director).not.toBe(movie.director);\n    expect(clonedMovie.director.name).toBe('Christopher Nolan');\n\n    clonedMovie.director.name = 'Christopher Nolan 2';\n\n    expect(clonedMovie.director.name).toBe('Christopher Nolan 2');\n    expect(movie.director.name).toBe('Christopher Nolan');\n  });\n\n  test('Identifiable component', async () => {\n    class Movie extends Component {\n      @primaryIdentifier() id!: string;\n      @attribute('string') title = '';\n    }\n\n    const movie = new Movie({title: 'Inception'});\n\n    const clonedMovie = movie.clone();\n\n    expect(clonedMovie).toBe(movie);\n  });\n\n  test('Referenced identifiable component', async () => {\n    class Director extends Component {\n      @primaryIdentifier() id!: string;\n      @attribute('string') name = '';\n    }\n\n    class Movie extends Component {\n      @provide() static Director = Director;\n\n      @attribute('Director') director!: Director;\n    }\n\n    const movie = new Movie({director: new Director({name: 'Christopher Nolan'})});\n\n    const clonedMovie = movie.clone();\n\n    expect(clonedMovie).not.toBe(movie);\n    expect(clonedMovie.director).toBe(movie.director);\n  });\n});\n"
  },
  {
    "path": "packages/component/src/cloning.ts",
    "content": "import {clone as simpleClone, CloneOptions} from 'simple-cloning';\n\nimport {isComponentClass, isComponentInstance} from './utilities';\n\nexport {CloneOptions};\n\n/**\n * Deeply clones any type of values including objects, arrays, and component instances (using Component's [`clone()`](https://layrjs.com/docs/v2/reference/component#clone-instance-method) instance method).\n *\n * @param value A value of any type.\n *\n * @returns A clone of the specified value.\n *\n * @example\n * ```\n * import {clone} from '﹫layr/component';\n *\n * const data = {\n *   token: 'xyz123',\n *   timestamp: 1596600889609,\n *   movie: new Movie({title: 'Inception'})\n * };\n *\n * const dataClone = clone(data);\n * dataClone.token; // => 'xyz123';\n * dataClone.timestamp; // => 1596600889609\n * dataClone.movie; // => A clone of data.movie\n * ```\n *\n * @category Cloning\n * @possiblyasync\n */\nexport function clone(value: any, options: CloneOptions = {}): any {\n  const {objectCloner: originalObjectCloner, ...otherOptions} = options;\n\n  const objectCloner = function (object: object): object | void {\n    if (originalObjectCloner !== undefined) {\n      const clonedObject = originalObjectCloner(object);\n\n      if (clonedObject !== undefined) {\n        return clonedObject;\n      }\n    }\n\n    if (isComponentClass(object)) {\n      return object.clone();\n    }\n\n    if (isComponentInstance(object)) {\n      return object.clone(options);\n    }\n  };\n\n  return simpleClone(value, {...otherOptions, objectCloner});\n}\n"
  },
  {
    "path": "packages/component/src/component.test.ts",
    "content": "import type {ExtendedError} from '@layr/utilities';\nimport sleep from 'sleep-promise';\n\nimport {Component} from './component';\nimport {EmbeddedComponent} from './embedded-component';\nimport {\n  isPropertyInstance,\n  Attribute,\n  isAttributeInstance,\n  Method,\n  isMethodInstance\n} from './properties';\nimport {validators} from './validation';\nimport {attribute, method, expose, provide, consume} from './decorators';\nimport {isComponentClass, isComponentInstance} from './utilities';\n\ndescribe('Component', () => {\n  describe('Creation', () => {\n    test('new ()', async () => {\n      class Movie extends Component {\n        static classAttribute = 1;\n\n        instanceAttribute = 2;\n\n        title!: string;\n        country!: string;\n      }\n\n      Movie.prototype.setAttribute('title', {valueType: 'string', default: ''});\n      Movie.prototype.setAttribute('country', {valueType: 'string', default: ''});\n\n      expect(isComponentClass(Movie)).toBe(true);\n      expect(Object.keys(Movie)).toEqual(['classAttribute']);\n      expect(Movie.classAttribute).toBe(1);\n\n      let movie = new Movie();\n\n      expect(isComponentInstance(movie)).toBe(true);\n      expect(movie).toBeInstanceOf(Movie);\n      expect(Object.keys(movie)).toEqual(['instanceAttribute']);\n      expect(movie.instanceAttribute).toBe(2);\n\n      expect(movie.title).toBe('');\n      expect(movie.country).toBe('');\n\n      movie = new Movie({title: 'Inception'});\n\n      expect(movie.title).toBe('Inception');\n      expect(movie.country).toBe('');\n\n      Movie.prototype.getAttribute('country').markAsControlled();\n\n      movie = new Movie();\n\n      expect(movie.title).toBe('');\n      expect(movie.getAttribute('country').isSet()).toBe(false);\n    });\n\n    test('instantiate()', async () => {\n      class Movie extends Component {\n        title!: string;\n        country!: string;\n      }\n\n      Movie.prototype.setAttribute('title', {valueType: 'string', default: ''});\n      Movie.prototype.setAttribute('country', {valueType: 'string', default: ''});\n\n      let movie = Movie.instantiate();\n\n      expect(movie.isNew()).toBe(false);\n      expect(movie.getAttribute('title').isSet()).toBe(false);\n      expect(movie.getAttribute('country').isSet()).toBe(false);\n\n      expect(() => Movie.instantiate({title: 'Inception'})).toThrow(\n        \"Cannot instantiate an unidentifiable component with an identifier (component: 'Movie')\"\n      );\n    });\n  });\n\n  describe('Initialization', () => {\n    test('Synchronous initializer', async () => {\n      class Movie extends Component {\n        static isInitialized = false;\n\n        static initializer() {\n          this.isInitialized = true;\n        }\n      }\n\n      expect(Movie.isInitialized).toBe(false);\n\n      Movie.initialize();\n\n      expect(Movie.isInitialized).toBe(true);\n    });\n\n    test('Asynchronous initializer', async () => {\n      class Movie extends Component {\n        static isInitialized = false;\n\n        static async initializer() {\n          await sleep(10);\n          this.isInitialized = true;\n        }\n      }\n\n      expect(Movie.isInitialized).toBe(false);\n\n      await Movie.initialize();\n\n      expect(Movie.isInitialized).toBe(true);\n    });\n  });\n\n  describe('Naming', () => {\n    test('getComponentName() and setComponentName()', async () => {\n      class Movie extends Component {}\n\n      expect(Movie.getComponentName()).toBe('Movie');\n\n      Movie.setComponentName('Film');\n\n      expect(Movie.getComponentName()).toBe('Film');\n\n      Movie.setComponentName('MotionPicture');\n\n      expect(Movie.getComponentName()).toBe('MotionPicture');\n\n      // Make sure there are no enumerable properties\n      expect(Object.keys(Movie)).toHaveLength(0);\n\n      expect(() => Movie.setComponentName('')).toThrow('A component name cannot be empty');\n      expect(() => Movie.setComponentName('1Place')).toThrow(\n        \"The specified component name ('1Place') is invalid\"\n      );\n      expect(() => Movie.setComponentName('motionPicture')).toThrow(\n        \"The specified component name ('motionPicture') is invalid\"\n      );\n      expect(() => Movie.setComponentName('MotionPicture!')).toThrow(\n        \"The specified component name ('MotionPicture!') is invalid\"\n      );\n    });\n\n    test('getComponentPath()', async () => {\n      class MovieDetails extends Component {}\n\n      class Movie extends Component {\n        @provide() static MovieDetails = MovieDetails;\n      }\n\n      class App extends Component {\n        @provide() static Movie = Movie;\n      }\n\n      expect(App.getComponentPath()).toBe('App');\n      expect(Movie.getComponentPath()).toBe('App.Movie');\n      expect(MovieDetails.getComponentPath()).toBe('App.Movie.MovieDetails');\n    });\n\n    test('getComponentType()', async () => {\n      class Movie extends Component {}\n\n      expect(Movie.getComponentType()).toBe('typeof Movie');\n      expect(Movie.prototype.getComponentType()).toBe('Movie');\n\n      Movie.setComponentName('Film');\n\n      expect(Movie.getComponentType()).toBe('typeof Film');\n      expect(Movie.prototype.getComponentType()).toBe('Film');\n    });\n  });\n\n  describe('isNew mark', () => {\n    test('isNew(), markAsNew(), and markAsNotNew()', async () => {\n      class Movie extends Component {}\n\n      const movie = new Movie();\n\n      expect(movie.isNew()).toBe(true);\n\n      movie.markAsNotNew();\n\n      expect(movie.isNew()).toBe(false);\n\n      movie.markAsNew();\n\n      expect(movie.isNew()).toBe(true);\n    });\n  });\n\n  describe('Properties', () => {\n    test('getProperty()', async () => {\n      class Movie extends Component {\n        @attribute() static limit = 100;\n\n        @method() static find() {}\n\n        @attribute() title = '';\n      }\n\n      let property = Movie.getProperty('limit');\n\n      expect(isPropertyInstance(property)).toBe(true);\n      expect(property.getName()).toBe('limit');\n      expect(property.getParent()).toBe(Movie);\n\n      property = Movie.getProperty('find');\n\n      expect(isPropertyInstance(property)).toBe(true);\n      expect(property.getName()).toBe('find');\n      expect(property.getParent()).toBe(Movie);\n\n      class Film extends Movie {}\n\n      property = Film.getProperty('limit');\n\n      expect(isPropertyInstance(property)).toBe(true);\n      expect(property.getName()).toBe('limit');\n      expect(property.getParent()).toBe(Film);\n\n      property = Film.getProperty('find');\n\n      expect(isPropertyInstance(property)).toBe(true);\n      expect(property.getName()).toBe('find');\n      expect(property.getParent()).toBe(Film);\n\n      const movie = new Movie();\n\n      property = movie.getProperty('title');\n\n      expect(isPropertyInstance(property)).toBe(true);\n      expect(property.getName()).toBe('title');\n      expect(property.getParent()).toBe(movie);\n\n      const film = new Film();\n\n      property = film.getProperty('title');\n\n      expect(isPropertyInstance(property)).toBe(true);\n      expect(property.getName()).toBe('title');\n      expect(property.getParent()).toBe(film);\n\n      expect(Movie.hasProperty('offset')).toBe(false);\n      expect(() => Movie.getProperty('offset')).toThrow(\n        \"The property 'offset' is missing (component: 'Movie')\"\n      );\n    });\n\n    test('hasProperty()', async () => {\n      class Movie extends Component {\n        @attribute() static limit = 100;\n\n        @method() static find() {}\n      }\n\n      expect(Movie.hasProperty('limit')).toBe(true);\n      expect(Movie.hasProperty('find')).toBe(true);\n      expect(Movie.hasProperty('offset')).toBe(false);\n      expect(Movie.prototype.hasProperty('limit')).toBe(false);\n    });\n\n    test('setProperty()', async () => {\n      class Movie extends Component {}\n\n      expect(Movie.hasProperty('limit')).toBe(false);\n\n      let setPropertyResult: any = Movie.setProperty('limit', Attribute);\n\n      expect(Movie.hasProperty('limit')).toBe(true);\n\n      let property = Movie.getProperty('limit');\n\n      expect(property).toBe(setPropertyResult);\n      expect(isPropertyInstance(property)).toBe(true);\n      expect(isAttributeInstance(property)).toBe(true);\n      expect(property.getName()).toBe('limit');\n      expect(property.getParent()).toBe(Movie);\n\n      expect(Movie.hasProperty('find')).toBe(false);\n\n      setPropertyResult = Movie.setProperty('find', Method);\n\n      expect(Movie.hasProperty('find')).toBe(true);\n\n      property = Movie.getProperty('find');\n\n      expect(property).toBe(setPropertyResult);\n      expect(isPropertyInstance(property)).toBe(true);\n      expect(isMethodInstance(property)).toBe(true);\n      expect(property.getName()).toBe('find');\n      expect(property.getParent()).toBe(Movie);\n\n      class Film extends Movie {}\n\n      expect(Film.hasProperty('limit')).toBe(true);\n\n      setPropertyResult = Film.setProperty('limit', Attribute);\n\n      expect(Film.hasProperty('limit')).toBe(true);\n\n      property = Film.getProperty('limit');\n\n      expect(property).toBe(setPropertyResult);\n      expect(isPropertyInstance(property)).toBe(true);\n      expect(property.getName()).toBe('limit');\n      expect(property.getParent()).toBe(Film);\n    });\n\n    test('getProperties()', async () => {\n      class Movie extends Component {\n        @attribute() title = '';\n        @attribute() duration = 0;\n\n        @method() load() {}\n        @method() save() {}\n      }\n\n      const movie = new Movie();\n\n      let properties = movie.getProperties();\n\n      expect(typeof properties[Symbol.iterator]).toBe('function');\n      expect(Array.from(properties).map((property) => property.getName())).toEqual([\n        'title',\n        'duration',\n        'load',\n        'save'\n      ]);\n\n      const classProperties = Movie.getProperties();\n\n      expect(typeof classProperties[Symbol.iterator]).toBe('function');\n      expect(Array.from(classProperties)).toHaveLength(0);\n\n      class Film extends Movie {\n        @attribute() director?: {name: string};\n      }\n\n      const film = new Film();\n\n      properties = film.getProperties();\n\n      expect(typeof properties[Symbol.iterator]).toBe('function');\n      expect(Array.from(properties).map((property) => property.getName())).toEqual([\n        'title',\n        'duration',\n        'load',\n        'save',\n        'director'\n      ]);\n\n      properties = film.getProperties({\n        filter(property) {\n          expect(property.getParent() === this);\n          return property.getName() !== 'duration';\n        }\n      });\n\n      expect(typeof properties[Symbol.iterator]).toBe('function');\n      expect(Array.from(properties).map((property) => property.getName())).toEqual([\n        'title',\n        'load',\n        'save',\n        'director'\n      ]);\n    });\n\n    test('getPropertyNames()', async () => {\n      class Movie extends Component {\n        @attribute() title = '';\n        @attribute() duration = 0;\n\n        @method() load() {}\n        @method() save() {}\n      }\n\n      expect(Movie.getPropertyNames()).toHaveLength(0);\n\n      expect(Movie.prototype.getPropertyNames()).toEqual(['title', 'duration', 'load', 'save']);\n\n      const movie = new Movie();\n\n      expect(movie.getPropertyNames()).toEqual(['title', 'duration', 'load', 'save']);\n\n      class Film extends Movie {\n        @attribute() director?: {name: string};\n      }\n\n      const film = new Film();\n\n      expect(film.getPropertyNames()).toEqual(['title', 'duration', 'load', 'save', 'director']);\n    });\n  });\n\n  describe('Attributes', () => {\n    test('getAttribute()', async () => {\n      class Movie extends Component {\n        @attribute() static limit = 100;\n\n        @method() static find() {}\n\n        @attribute() title = '';\n      }\n\n      let attr = Movie.getAttribute('limit');\n\n      expect(isAttributeInstance(attr)).toBe(true);\n      expect(attr.getName()).toBe('limit');\n      expect(attr.getParent()).toBe(Movie);\n\n      expect(() => Movie.getAttribute('find')).toThrow(\n        \"A property with the specified name was found, but it is not an attribute (method: 'Movie.find')\"\n      );\n\n      class Film extends Movie {}\n\n      attr = Film.getAttribute('limit');\n\n      expect(isAttributeInstance(attr)).toBe(true);\n      expect(attr.getName()).toBe('limit');\n      expect(attr.getParent()).toBe(Film);\n\n      const movie = new Movie();\n\n      const instanceAttribute = movie.getAttribute('title');\n\n      expect(isAttributeInstance(instanceAttribute)).toBe(true);\n      expect(instanceAttribute.getName()).toBe('title');\n      expect(instanceAttribute.getParent()).toBe(movie);\n\n      const film = new Film();\n\n      attr = film.getAttribute('title');\n\n      expect(isAttributeInstance(attr)).toBe(true);\n      expect(attr.getName()).toBe('title');\n      expect(attr.getParent()).toBe(film);\n\n      expect(Movie.hasAttribute('offset')).toBe(false);\n      expect(() => Movie.getAttribute('offset')).toThrow(\n        \"The attribute 'offset' is missing (component: 'Movie')\"\n      );\n    });\n\n    test('hasAttribute()', async () => {\n      class Movie extends Component {\n        @attribute() static limit = 100;\n\n        @method() static find() {}\n      }\n\n      expect(Movie.hasAttribute('limit')).toBe(true);\n      expect(Movie.hasAttribute('offset')).toBe(false);\n      expect(Movie.prototype.hasAttribute('limit')).toBe(false);\n\n      expect(() => Movie.hasAttribute('find')).toThrow(\n        \"A property with the specified name was found, but it is not an attribute (method: 'Movie.find')\"\n      );\n    });\n\n    test('setAttribute()', async () => {\n      class Movie extends Component {\n        @method() static find() {}\n      }\n\n      expect(Movie.hasAttribute('limit')).toBe(false);\n\n      let setAttributeResult = Movie.setAttribute('limit');\n\n      expect(Movie.hasAttribute('limit')).toBe(true);\n\n      let attr = Movie.getAttribute('limit');\n\n      expect(attr).toBe(setAttributeResult);\n      expect(isAttributeInstance(attr)).toBe(true);\n      expect(attr.getName()).toBe('limit');\n      expect(attr.getParent()).toBe(Movie);\n\n      expect(() => Movie.setAttribute('find')).toThrow(\n        \"Cannot change the type of a property (method: 'Movie.find')\"\n      );\n\n      class Film extends Movie {}\n\n      expect(Film.hasAttribute('limit')).toBe(true);\n\n      setAttributeResult = Film.setAttribute('limit');\n\n      expect(Film.hasAttribute('limit')).toBe(true);\n\n      attr = Film.getAttribute('limit');\n\n      expect(attr).toBe(setAttributeResult);\n      expect(isAttributeInstance(attr)).toBe(true);\n      expect(attr.getName()).toBe('limit');\n      expect(attr.getParent()).toBe(Film);\n\n      expect(() => Film.setAttribute('find')).toThrow(\n        \"Cannot change the type of a property (method: 'Film.find')\"\n      );\n    });\n\n    test('getAttributes()', async () => {\n      class Movie extends Component {\n        @attribute() title = '';\n        @attribute() duration = 0;\n\n        @method() load() {}\n        @method() save() {}\n      }\n\n      const movie = new Movie();\n\n      let attributes = movie.getAttributes();\n\n      expect(typeof attributes[Symbol.iterator]).toBe('function');\n\n      expect(Array.from(attributes).map((property) => property.getName())).toEqual([\n        'title',\n        'duration'\n      ]);\n\n      attributes = Movie.getAttributes();\n\n      expect(typeof attributes[Symbol.iterator]).toBe('function');\n      expect(Array.from(attributes)).toHaveLength(0);\n\n      class Film extends Movie {\n        @attribute() director?: string;\n      }\n\n      const film = Film.instantiate();\n      film.title = 'Inception';\n      film.director = 'Christopher Nolan';\n\n      attributes = film.getAttributes();\n\n      expect(Array.from(attributes).map((property) => property.getName())).toEqual([\n        'title',\n        'duration',\n        'director'\n      ]);\n\n      attributes = film.getAttributes({attributeSelector: {title: true, director: true}});\n\n      expect(Array.from(attributes).map((property) => property.getName())).toEqual([\n        'title',\n        'director'\n      ]);\n\n      attributes = film.getAttributes({setAttributesOnly: true});\n\n      expect(Array.from(attributes).map((property) => property.getName())).toEqual([\n        'title',\n        'director'\n      ]);\n\n      attributes = film.getAttributes({\n        filter(property) {\n          expect(property.getParent() === this);\n          return property.getName() !== 'duration';\n        }\n      });\n\n      expect(Array.from(attributes).map((property) => property.getName())).toEqual([\n        'title',\n        'director'\n      ]);\n    });\n\n    test('traverseAttributes()', async () => {\n      class Article extends EmbeddedComponent {\n        @attribute('string') title = '';\n      }\n\n      class Blog extends Component {\n        @provide() static Article = Article;\n\n        @attribute('string') name = '';\n        @attribute('Article[]') articles = new Array<Article>();\n      }\n\n      const traverseAttributes = (component: Component, options?: any) => {\n        const traversedAttributes = new Array<Attribute>();\n        component.traverseAttributes((attribute) => {\n          traversedAttributes.push(attribute);\n        }, options);\n        return traversedAttributes;\n      };\n\n      expect(traverseAttributes(Blog.prototype)).toEqual([\n        Blog.prototype.getAttribute('name'),\n        Blog.prototype.getAttribute('articles'),\n        Article.prototype.getAttribute('title')\n      ]);\n\n      expect(traverseAttributes(Blog.prototype, {attributeSelector: {}})).toEqual([]);\n\n      expect(traverseAttributes(Blog.prototype, {attributeSelector: {name: true}})).toEqual([\n        Blog.prototype.getAttribute('name')\n      ]);\n\n      expect(traverseAttributes(Blog.prototype, {attributeSelector: {articles: {}}})).toEqual([\n        Blog.prototype.getAttribute('articles')\n      ]);\n\n      expect(\n        traverseAttributes(Blog.prototype, {attributeSelector: {articles: {title: true}}})\n      ).toEqual([Blog.prototype.getAttribute('articles'), Article.prototype.getAttribute('title')]);\n\n      const blog = Blog.instantiate();\n\n      expect(traverseAttributes(blog, {setAttributesOnly: true})).toEqual([]);\n\n      blog.name = 'The Blog';\n\n      expect(traverseAttributes(blog, {setAttributesOnly: true})).toEqual([\n        blog.getAttribute('name')\n      ]);\n\n      blog.articles = [];\n\n      expect(traverseAttributes(blog, {setAttributesOnly: true})).toEqual([\n        blog.getAttribute('name'),\n        blog.getAttribute('articles')\n      ]);\n\n      const article1 = Article.instantiate();\n      blog.articles.push(article1);\n\n      expect(traverseAttributes(blog, {setAttributesOnly: true})).toEqual([\n        blog.getAttribute('name'),\n        blog.getAttribute('articles')\n      ]);\n\n      article1.title = 'First Article';\n\n      expect(traverseAttributes(blog, {setAttributesOnly: true})).toEqual([\n        blog.getAttribute('name'),\n        blog.getAttribute('articles'),\n        article1.getAttribute('title')\n      ]);\n\n      const article2 = Article.instantiate();\n      blog.articles.push(article2);\n\n      expect(traverseAttributes(blog, {setAttributesOnly: true})).toEqual([\n        blog.getAttribute('name'),\n        blog.getAttribute('articles'),\n        article1.getAttribute('title')\n      ]);\n\n      article2.title = 'Second Article';\n\n      expect(traverseAttributes(blog, {setAttributesOnly: true})).toEqual([\n        blog.getAttribute('name'),\n        blog.getAttribute('articles'),\n        article1.getAttribute('title'),\n        article2.getAttribute('title')\n      ]);\n    });\n\n    test('resolveAttributeSelector()', async () => {\n      class Person extends EmbeddedComponent {\n        @attribute('string') name = '';\n      }\n\n      class Movie extends Component {\n        @provide() static Person = Person;\n\n        @attribute('string') title = '';\n        @attribute('number') duration = 0;\n        @attribute('Person?') director?: Person;\n        @attribute('Person[]') actors: Person[] = [];\n      }\n\n      expect(Movie.prototype.resolveAttributeSelector(true)).toStrictEqual({\n        title: true,\n        duration: true,\n        director: {name: true},\n        actors: {name: true}\n      });\n\n      expect(Movie.prototype.resolveAttributeSelector(false)).toStrictEqual({});\n\n      expect(Movie.prototype.resolveAttributeSelector({})).toStrictEqual({});\n\n      expect(Movie.prototype.resolveAttributeSelector({title: true, director: true})).toStrictEqual(\n        {\n          title: true,\n          director: {name: true}\n        }\n      );\n\n      expect(\n        Movie.prototype.resolveAttributeSelector({title: true, director: false})\n      ).toStrictEqual({\n        title: true\n      });\n\n      expect(Movie.prototype.resolveAttributeSelector({title: true, director: {}})).toStrictEqual({\n        title: true,\n        director: {}\n      });\n\n      expect(Movie.prototype.resolveAttributeSelector(true, {depth: 0})).toStrictEqual({\n        title: true,\n        duration: true,\n        director: true,\n        actors: true\n      });\n\n      expect(\n        Movie.prototype.resolveAttributeSelector({title: true, actors: true}, {depth: 0})\n      ).toStrictEqual({title: true, actors: true});\n\n      const movie = Movie.instantiate();\n\n      expect(movie.resolveAttributeSelector(true, {setAttributesOnly: true})).toStrictEqual({});\n\n      movie.getAttribute('title').setValue('Interception', {source: 'client'});\n      movie.getAttribute('duration').setValue(120, {source: 'client'});\n\n      expect(movie.resolveAttributeSelector(true, {setAttributesOnly: true})).toStrictEqual({\n        title: true,\n        duration: true\n      });\n\n      expect(\n        movie.resolveAttributeSelector(true, {setAttributesOnly: true, target: 'client'})\n      ).toStrictEqual({});\n\n      movie.getAttribute('title').setValue('Interception 2', {source: 'local'});\n\n      expect(\n        movie.resolveAttributeSelector(true, {setAttributesOnly: true, target: 'client'})\n      ).toStrictEqual({title: true});\n\n      // --- With an embedded component ---\n\n      class UserDetails extends EmbeddedComponent {\n        @attribute('string') country = '';\n      }\n\n      class User extends Component {\n        @provide() static UserDetails = UserDetails;\n\n        @attribute('string') name = '';\n        @attribute('UserDetails?') details?: UserDetails;\n      }\n\n      expect(User.prototype.resolveAttributeSelector(true)).toStrictEqual({\n        name: true,\n        details: {country: true}\n      });\n\n      const user = User.instantiate();\n\n      expect(user.resolveAttributeSelector(true, {setAttributesOnly: true})).toStrictEqual({});\n\n      user.name = 'John';\n\n      expect(user.resolveAttributeSelector(true, {setAttributesOnly: true})).toStrictEqual({\n        name: true\n      });\n\n      user.details = UserDetails.instantiate();\n\n      expect(user.resolveAttributeSelector(true, {setAttributesOnly: true})).toStrictEqual({\n        name: true,\n        details: {}\n      });\n\n      user.details.country = 'USA';\n\n      expect(user.resolveAttributeSelector(true, {setAttributesOnly: true})).toStrictEqual({\n        name: true,\n        details: {country: true}\n      });\n\n      // --- With an array of embedded components ---\n\n      class Article extends EmbeddedComponent {\n        @attribute('string') title = '';\n      }\n\n      class Blog extends Component {\n        @provide() static Article = Article;\n\n        @attribute('string') name = '';\n        @attribute('Article[]') articles = new Array<Article>();\n      }\n\n      expect(Blog.prototype.resolveAttributeSelector(true)).toStrictEqual({\n        name: true,\n        articles: {title: true}\n      });\n\n      expect(Blog.prototype.resolveAttributeSelector({articles: {}})).toStrictEqual({\n        articles: {}\n      });\n\n      expect(\n        Blog.prototype.resolveAttributeSelector({articles: {}}, {allowPartialArrayItems: false})\n      ).toStrictEqual({\n        articles: {title: true}\n      });\n\n      const blog = Blog.instantiate();\n\n      expect(blog.resolveAttributeSelector(true, {setAttributesOnly: true})).toStrictEqual({});\n\n      blog.name = 'The Blog';\n\n      expect(blog.resolveAttributeSelector(true, {setAttributesOnly: true})).toStrictEqual({\n        name: true\n      });\n\n      blog.articles = [];\n\n      expect(blog.resolveAttributeSelector(true, {setAttributesOnly: true})).toStrictEqual({\n        name: true,\n        articles: {}\n      });\n\n      const article1 = Article.instantiate();\n\n      blog.articles.push(article1);\n\n      expect(blog.resolveAttributeSelector(true, {setAttributesOnly: true})).toStrictEqual({\n        name: true,\n        articles: {}\n      });\n\n      article1.title = 'First Article';\n\n      expect(blog.resolveAttributeSelector(true, {setAttributesOnly: true})).toStrictEqual({\n        name: true,\n        articles: {title: true}\n      });\n\n      const article2 = Article.instantiate();\n\n      blog.articles.push(article2);\n\n      expect(blog.resolveAttributeSelector(true, {setAttributesOnly: true})).toStrictEqual({\n        name: true,\n        articles: {title: true}\n      });\n\n      expect(\n        blog.resolveAttributeSelector(true, {\n          setAttributesOnly: true,\n          aggregationMode: 'intersection'\n        })\n      ).toStrictEqual({\n        name: true,\n        articles: {}\n      });\n\n      article2.title = 'Second Article';\n\n      expect(blog.resolveAttributeSelector(true, {setAttributesOnly: true})).toStrictEqual({\n        name: true,\n        articles: {title: true}\n      });\n\n      expect(\n        blog.resolveAttributeSelector(true, {\n          setAttributesOnly: true,\n          aggregationMode: 'intersection'\n        })\n      ).toStrictEqual({\n        name: true,\n        articles: {title: true}\n      });\n    });\n\n    test('Validation', async () => {\n      const notEmpty = validators.notEmpty();\n      const maxLength = validators.maxLength(3);\n\n      class Person extends EmbeddedComponent {\n        @attribute('string', {validators: [notEmpty]}) name = '';\n        @attribute('string?') country?: string;\n      }\n\n      class Movie extends Component {\n        @provide() static Person = Person;\n\n        @attribute('string', {validators: [notEmpty]}) title = '';\n        @attribute('string[]', {\n          validators: [maxLength],\n          items: {validators: [notEmpty]}\n        })\n        tags: string[] = [];\n        @attribute('Person?') director?: Person;\n        @attribute('Person[]') actors: Person[] = [];\n      }\n\n      const movie = new Movie();\n\n      expect(() => movie.validate()).toThrow(\n        \"The following error(s) occurred while validating the component 'Movie': The validator `notEmpty()` failed (path: 'title')\"\n      );\n      expect(movie.isValid()).toBe(false);\n      expect(movie.runValidators()).toEqual([{validator: notEmpty, path: 'title'}]);\n      expect(movie.runValidators({title: true})).toEqual([{validator: notEmpty, path: 'title'}]);\n      expect(movie.runValidators({tags: true})).toEqual([]);\n\n      movie.title = 'Inception';\n\n      expect(() => movie.validate()).not.toThrow();\n      expect(movie.isValid()).toBe(true);\n      expect(movie.runValidators()).toEqual([]);\n\n      movie.tags = ['action'];\n\n      expect(() => movie.validate()).not.toThrow();\n      expect(movie.isValid()).toBe(true);\n      expect(movie.runValidators()).toEqual([]);\n\n      movie.tags.push('adventure');\n\n      expect(() => movie.validate()).not.toThrow();\n      expect(movie.isValid()).toBe(true);\n      expect(movie.runValidators()).toEqual([]);\n\n      movie.tags.push('');\n\n      expect(() => movie.validate()).toThrow(\n        \"The following error(s) occurred while validating the component 'Movie': The validator `notEmpty()` failed (path: 'tags[2]')\"\n      );\n      expect(movie.isValid()).toBe(false);\n      expect(movie.runValidators()).toEqual([{validator: notEmpty, path: 'tags[2]'}]);\n      expect(movie.runValidators({tags: true})).toEqual([{validator: notEmpty, path: 'tags[2]'}]);\n      expect(movie.runValidators({title: true})).toEqual([]);\n\n      movie.tags.push('sci-fi');\n\n      expect(() => movie.validate()).toThrow(\n        \"The following error(s) occurred while validating the component 'Movie': The validator `maxLength(3)` failed (path: 'tags'), The validator `notEmpty()` failed (path: 'tags[2]')\"\n      );\n      expect(movie.isValid()).toBe(false);\n      expect(movie.runValidators()).toEqual([\n        {validator: maxLength, path: 'tags'},\n        {validator: notEmpty, path: 'tags[2]'}\n      ]);\n\n      movie.tags.splice(2, 1);\n\n      movie.director = new Person();\n\n      expect(() => movie.validate()).toThrow(\n        \"The following error(s) occurred while validating the component 'Movie': The validator `notEmpty()` failed (path: 'director.name')\"\n      );\n      expect(movie.isValid()).toBe(false);\n      expect(movie.runValidators()).toEqual([{validator: notEmpty, path: 'director.name'}]);\n      expect(movie.runValidators({director: {name: true}})).toEqual([\n        {validator: notEmpty, path: 'director.name'}\n      ]);\n      expect(movie.runValidators({director: {country: true}})).toEqual([]);\n\n      movie.director.name = 'Christopher Nolan';\n\n      expect(() => movie.validate()).not.toThrow();\n      expect(movie.isValid()).toBe(true);\n      expect(movie.runValidators()).toEqual([]);\n\n      movie.actors.push(new Person());\n\n      expect(() => movie.validate()).toThrow(\n        \"The following error(s) occurred while validating the component 'Movie': The validator `notEmpty()` failed (path: 'actors[0].name')\"\n      );\n      expect(movie.isValid()).toBe(false);\n      expect(movie.runValidators()).toEqual([{validator: notEmpty, path: 'actors[0].name'}]);\n\n      movie.actors[0].name = 'Leonardo DiCaprio';\n\n      expect(() => movie.validate()).not.toThrow();\n      expect(movie.isValid()).toBe(true);\n      expect(movie.runValidators()).toEqual([]);\n\n      // --- With a custom message ---\n\n      class Cinema extends Component {\n        @attribute('string', {validators: [validators.notEmpty('The name cannot be empty.')]})\n        name = '';\n      }\n\n      const cinema = new Cinema({name: 'Paradiso'});\n\n      expect(() => cinema.validate()).not.toThrow();\n\n      cinema.name = '';\n\n      let error: ExtendedError;\n\n      try {\n        cinema.validate();\n      } catch (err: any) {\n        error = err;\n      }\n\n      expect(error!.message).toBe(\n        \"The following error(s) occurred while validating the component 'Cinema': The name cannot be empty. (path: 'name')\"\n      );\n\n      expect(error!.displayMessage).toBe('The name cannot be empty.');\n    });\n  });\n\n  describe('Methods', () => {\n    test('getMethod()', async () => {\n      class Movie extends Component {\n        @attribute() static limit = 100;\n\n        @method() static find() {}\n\n        @method() load() {}\n      }\n\n      let meth = Movie.getMethod('find');\n\n      expect(isMethodInstance(meth)).toBe(true);\n      expect(meth.getName()).toBe('find');\n      expect(meth.getParent()).toBe(Movie);\n\n      expect(() => Movie.getMethod('limit')).toThrow(\n        \"A property with the specified name was found, but it is not a method (attribute: 'Movie.limit')\"\n      );\n\n      class Film extends Movie {}\n\n      meth = Film.getMethod('find');\n\n      expect(isMethodInstance(meth)).toBe(true);\n      expect(meth.getName()).toBe('find');\n      expect(meth.getParent()).toBe(Film);\n\n      const movie = new Movie();\n\n      meth = movie.getMethod('load');\n\n      expect(isMethodInstance(meth)).toBe(true);\n      expect(meth.getName()).toBe('load');\n      expect(meth.getParent()).toBe(movie);\n\n      const film = new Film();\n\n      meth = film.getMethod('load');\n\n      expect(isMethodInstance(meth)).toBe(true);\n      expect(meth.getName()).toBe('load');\n      expect(meth.getParent()).toBe(film);\n\n      expect(Movie.hasMethod('load')).toBe(false);\n      expect(() => Movie.getMethod('load')).toThrow(\n        \"The method 'load' is missing (component: 'Movie')\"\n      );\n    });\n\n    test('hasMethod()', async () => {\n      class Movie extends Component {\n        @attribute() static limit = 100;\n\n        @method() static find() {}\n      }\n\n      expect(Movie.hasMethod('find')).toBe(true);\n      expect(Movie.hasMethod('load')).toBe(false);\n      expect(Movie.prototype.hasMethod('find')).toBe(false);\n\n      expect(() => Movie.hasMethod('limit')).toThrow(\n        \"A property with the specified name was found, but it is not a method (attribute: 'Movie.limit')\"\n      );\n    });\n\n    test('setMethod()', async () => {\n      class Movie extends Component {\n        @attribute() static limit = 100;\n      }\n\n      expect(Movie.hasMethod('find')).toBe(false);\n\n      let setMethodResult = Movie.setMethod('find');\n\n      expect(Movie.hasMethod('find')).toBe(true);\n\n      let meth = Movie.getMethod('find');\n\n      expect(meth).toBe(setMethodResult);\n      expect(isMethodInstance(meth)).toBe(true);\n      expect(meth.getName()).toBe('find');\n      expect(meth.getParent()).toBe(Movie);\n\n      expect(() => Movie.setMethod('limit')).toThrow(\n        \"Cannot change the type of a property (attribute: 'Movie.limit')\"\n      );\n\n      class Film extends Movie {}\n\n      expect(Film.hasMethod('find')).toBe(true);\n\n      setMethodResult = Film.setMethod('find');\n\n      expect(Film.hasMethod('find')).toBe(true);\n\n      meth = Film.getMethod('find');\n\n      expect(meth).toBe(setMethodResult);\n      expect(isMethodInstance(meth)).toBe(true);\n      expect(meth.getName()).toBe('find');\n      expect(meth.getParent()).toBe(Film);\n\n      expect(() => Film.setMethod('limit')).toThrow(\n        \"Cannot change the type of a property (attribute: 'Film.limit')\"\n      );\n    });\n\n    test('getMethods()', async () => {\n      class Movie extends Component {\n        @attribute() title = '';\n        @attribute() duration = 0;\n\n        @method() load() {}\n        @method() save() {}\n      }\n\n      const movie = new Movie();\n\n      let methods = movie.getMethods();\n\n      expect(typeof methods[Symbol.iterator]).toBe('function');\n      expect(Array.from(methods).map((property) => property.getName())).toEqual(['load', 'save']);\n\n      methods = Movie.getMethods();\n\n      expect(typeof methods[Symbol.iterator]).toBe('function');\n      expect(Array.from(methods)).toHaveLength(0);\n\n      class Film extends Movie {\n        @method() delete() {}\n      }\n\n      const film = new Film();\n\n      methods = film.getMethods();\n\n      expect(typeof methods[Symbol.iterator]).toBe('function');\n      expect(Array.from(methods).map((property) => property.getName())).toEqual([\n        'load',\n        'save',\n        'delete'\n      ]);\n\n      methods = film.getMethods({\n        filter(property) {\n          expect(property.getParent() === this);\n          return property.getName() !== 'save';\n        }\n      });\n\n      expect(typeof methods[Symbol.iterator]).toBe('function');\n      expect(Array.from(methods).map((property) => property.getName())).toEqual(['load', 'delete']);\n    });\n  });\n\n  describe('Dependency management', () => {\n    test('Component getters', async () => {\n      class Movie extends Component {\n        @consume() static Director: typeof Director;\n      }\n\n      class Director extends Component {\n        @consume() static Movie: typeof Movie;\n      }\n\n      class App extends Component {\n        @provide() static Movie = Movie;\n        @provide() static Director = Director;\n      }\n\n      expect(App.getComponent('App')).toBe(App);\n      expect(App.getComponent('Movie')).toBe(Movie);\n      expect(App.getComponent('Director')).toBe(Director);\n      expect(Movie.getComponent('Director')).toBe(Director);\n      expect(Director.getComponent('Movie')).toBe(Movie);\n\n      expect(App.hasComponent('Movie')).toBe(true);\n      expect(App.hasComponent('Producer')).toBe(false);\n      expect(() => App.getComponent('Producer')).toThrow(\n        \"Cannot get the component 'Producer' from the component 'App'\"\n      );\n\n      expect(App.getComponentOfType('typeof Movie')).toBe(Movie);\n      expect(App.getComponentOfType('Movie')).toBe(Movie.prototype);\n\n      expect(App.hasComponentOfType('typeof Movie')).toBe(true);\n      expect(App.hasComponentOfType('Movie')).toBe(true);\n      expect(App.hasComponentOfType('typeof Producer')).toBe(false);\n      expect(App.hasComponentOfType('Producer')).toBe(false);\n      expect(() => App.getComponentOfType('typeof Producer')).toThrow(\n        \"Cannot get the component of type 'typeof Producer' from the component 'App'\"\n      );\n    });\n\n    test('Component provision', async () => {\n      class Movie extends Component {\n        static Director: typeof Director;\n        static Actor: typeof Actor;\n        static Producer: typeof Producer;\n      }\n\n      class Director extends Component {}\n\n      class Actor extends Component {}\n\n      class Producer extends Component {}\n\n      expect(Movie.getProvidedComponent('Director')).toBeUndefined();\n      expect(Movie.getProvidedComponent('Actor')).toBeUndefined();\n      expect(Movie.getProvidedComponent('Producer')).toBeUndefined();\n\n      expect(Movie.getComponentProvider()).toBe(Movie);\n      expect(Director.getComponentProvider()).toBe(Director);\n      expect(Actor.getComponentProvider()).toBe(Actor);\n\n      Movie.provideComponent(Director);\n      Movie.provideComponent(Actor);\n\n      // Providing the same component a second time should have no effects\n      Movie.provideComponent(Director);\n\n      expect(Movie.getProvidedComponent('Director')).toBe(Director);\n      expect(Movie.getProvidedComponent('Actor')).toBe(Actor);\n      expect(Movie.getProvidedComponent('Producer')).toBeUndefined();\n\n      // Provided components should also be accessible through component accessors\n      expect(Movie.Director).toBe(Director);\n      expect(Movie.Actor).toBe(Actor);\n      expect(Movie.Producer).toBe(undefined);\n\n      // And through `getComponent()`\n      expect(Movie.getComponent('Director')).toBe(Director);\n      expect(Movie.getComponent('Actor')).toBe(Actor);\n      expect(Movie.hasComponent('Producer')).toBe(false);\n\n      // It should be possible to get the component provider of a provided component\n      expect(Director.getComponent('Movie')).toBe(Movie);\n\n      expect(Array.from(Movie.getProvidedComponents())).toEqual([Director, Actor]);\n\n      expect(Movie.getComponentProvider()).toBe(Movie);\n      expect(Director.getComponentProvider()).toBe(Movie);\n      expect(Actor.getComponentProvider()).toBe(Movie);\n\n      const MovieFork = Movie.fork();\n\n      const DirectorFork = MovieFork.Director;\n\n      expect(DirectorFork?.isForkOf(Director)).toBe(true);\n      expect(DirectorFork).not.toBe(Director);\n\n      const SameDirectorFork = MovieFork.Director;\n\n      expect(SameDirectorFork).toBe(DirectorFork);\n\n      expect(DirectorFork.getComponentProvider()).toBe(MovieFork);\n\n      class Root extends Component {}\n\n      Root.provideComponent(Movie);\n\n      expect(Array.from(Root.getProvidedComponents())).toEqual([Movie]);\n      expect(Array.from(Root.getProvidedComponents({deep: true}))).toEqual([\n        Movie,\n        Director,\n        Actor\n      ]);\n\n      expect(Array.from(Root.traverseComponents())).toEqual([Root, Movie, Director, Actor]);\n\n      class Film extends Component {}\n\n      expect(() => Film.provideComponent(Director)).toThrow(\n        \"Cannot provide the component 'Director' from 'Film' because 'Director' is already provided by 'Movie'\"\n      );\n\n      class OtherComponent extends Component {}\n\n      OtherComponent.setComponentName('Director');\n\n      expect(() => Movie.provideComponent(OtherComponent)).toThrow(\n        \"Cannot provide the component 'Director' from 'Movie' because a component with the same name is already provided\"\n      );\n\n      (Movie as any).Producer = {};\n\n      expect(() => Movie.provideComponent(Producer)).toThrow(\n        \"Cannot provide the component 'Producer' from 'Movie' because there is an existing property with the same name\"\n      );\n    });\n\n    test('Component consumption', async () => {\n      class Movie extends Component {\n        static Director: typeof Director;\n        static Producer: typeof Producer;\n      }\n\n      class Director extends Component {\n        static Movie: typeof Movie;\n      }\n\n      class Producer extends Component {}\n\n      class App extends Component {\n        @provide() static Movie = Movie;\n        @provide() static Director = Director;\n        static Producer: typeof Producer;\n      }\n\n      expect(Movie.getConsumedComponent('Director')).toBeUndefined();\n      expect(Director.getConsumedComponent('Movie')).toBeUndefined();\n      expect(Movie.getConsumedComponent('Producer')).toBeUndefined();\n\n      expect(Movie.getComponentProvider()).toBe(App);\n      expect(Director.getComponentProvider()).toBe(App);\n\n      Movie.consumeComponent('Director');\n      Director.consumeComponent('Movie');\n\n      // Consuming the same component a second time should have no effects\n      Movie.consumeComponent('Director');\n\n      expect(Movie.getConsumedComponent('Director')).toBe(Director);\n      expect(Director.getConsumedComponent('Movie')).toBe(Movie);\n      expect(Movie.getConsumedComponent('Producer')).toBeUndefined();\n\n      // Consumed components should also be accessible through component accessors\n      expect(Movie.Director).toBe(Director);\n      expect(Director.Movie).toBe(Movie);\n      expect(Movie.Producer).toBe(undefined);\n\n      // And through `getComponent()`\n      expect(Movie.getComponent('Director')).toBe(Director);\n      expect(Director.getComponent('Movie')).toBe(Movie);\n      expect(Movie.hasComponent('Producer')).toBe(false);\n\n      expect(Array.from(Movie.getConsumedComponents())).toEqual([Director]);\n      expect(Array.from(Director.getConsumedComponents())).toEqual([Movie]);\n\n      const AppFork = App.fork();\n\n      const MovieFork = AppFork.Movie;\n      const DirectorFork = AppFork.Director;\n\n      expect(MovieFork?.isForkOf(Movie)).toBe(true);\n      expect(MovieFork).not.toBe(Movie);\n      expect(DirectorFork?.isForkOf(Director)).toBe(true);\n      expect(DirectorFork).not.toBe(Director);\n\n      const SameMovieFork = AppFork.Movie;\n      const SameDirectorFork = AppFork.Director;\n\n      expect(SameMovieFork).toBe(MovieFork);\n      expect(SameDirectorFork).toBe(DirectorFork);\n\n      expect(MovieFork.Director).toBe(DirectorFork);\n      expect(DirectorFork.Movie).toBe(MovieFork);\n\n      expect(MovieFork.getComponentProvider()).toBe(AppFork);\n      expect(DirectorFork.getComponentProvider()).toBe(AppFork);\n\n      (Movie as any).Producer = Producer;\n\n      expect(() => Movie.consumeComponent('Producer')).toThrow(\n        \"Cannot consume the component 'Producer' from 'Movie' because there is an existing property with the same name\"\n      );\n    });\n  });\n\n  describe('Attachment', () => {\n    test('attach(), detach(), isAttached(), and isDetached()', async () => {\n      class Movie extends Component {}\n\n      class App extends Component {\n        @provide() static Movie = Movie;\n      }\n\n      const movie = new Movie();\n      const otherMovie = new Movie();\n\n      expect(App.isAttached()).toBe(true);\n      expect(Movie.isAttached()).toBe(true);\n      expect(movie.isAttached()).toBe(true);\n      expect(otherMovie.isAttached()).toBe(true);\n      expect(App.isDetached()).toBe(false);\n      expect(Movie.isDetached()).toBe(false);\n      expect(movie.isDetached()).toBe(false);\n      expect(otherMovie.isDetached()).toBe(false);\n\n      App.detach();\n\n      expect(App.isAttached()).toBe(false);\n      expect(Movie.isAttached()).toBe(false);\n      expect(movie.isAttached()).toBe(false);\n      expect(otherMovie.isAttached()).toBe(false);\n      expect(App.isDetached()).toBe(true);\n      expect(Movie.isDetached()).toBe(true);\n      expect(movie.isDetached()).toBe(true);\n      expect(otherMovie.isDetached()).toBe(true);\n\n      App.attach();\n\n      expect(App.isAttached()).toBe(true);\n      expect(Movie.isAttached()).toBe(true);\n      expect(movie.isAttached()).toBe(true);\n      expect(otherMovie.isAttached()).toBe(true);\n      expect(App.isDetached()).toBe(false);\n      expect(Movie.isDetached()).toBe(false);\n      expect(movie.isDetached()).toBe(false);\n      expect(otherMovie.isDetached()).toBe(false);\n\n      Movie.detach();\n\n      expect(App.isAttached()).toBe(true);\n      expect(Movie.isAttached()).toBe(false);\n      expect(movie.isAttached()).toBe(false);\n      expect(otherMovie.isAttached()).toBe(false);\n      expect(App.isDetached()).toBe(false);\n      expect(Movie.isDetached()).toBe(true);\n      expect(movie.isDetached()).toBe(true);\n      expect(otherMovie.isDetached()).toBe(true);\n\n      Movie.attach();\n\n      expect(App.isAttached()).toBe(true);\n      expect(Movie.isAttached()).toBe(true);\n      expect(movie.isAttached()).toBe(true);\n      expect(otherMovie.isAttached()).toBe(true);\n      expect(App.isDetached()).toBe(false);\n      expect(Movie.isDetached()).toBe(false);\n      expect(movie.isDetached()).toBe(false);\n      expect(otherMovie.isDetached()).toBe(false);\n\n      movie.detach();\n\n      expect(App.isAttached()).toBe(true);\n      expect(Movie.isAttached()).toBe(true);\n      expect(movie.isAttached()).toBe(false);\n      expect(otherMovie.isAttached()).toBe(true);\n      expect(App.isDetached()).toBe(false);\n      expect(Movie.isDetached()).toBe(false);\n      expect(movie.isDetached()).toBe(true);\n      expect(otherMovie.isDetached()).toBe(false);\n\n      movie.attach();\n\n      expect(App.isAttached()).toBe(true);\n      expect(Movie.isAttached()).toBe(true);\n      expect(movie.isAttached()).toBe(true);\n      expect(otherMovie.isAttached()).toBe(true);\n      expect(App.isDetached()).toBe(false);\n      expect(Movie.isDetached()).toBe(false);\n      expect(movie.isDetached()).toBe(false);\n      expect(otherMovie.isDetached()).toBe(false);\n\n      App.detach();\n\n      // Since Movie and movie has been explicitly attached,\n      // they should remain so even though App is detached\n      expect(App.isAttached()).toBe(false);\n      expect(Movie.isAttached()).toBe(true);\n      expect(movie.isAttached()).toBe(true);\n      expect(otherMovie.isAttached()).toBe(true);\n      expect(App.isDetached()).toBe(true);\n      expect(Movie.isDetached()).toBe(false);\n      expect(movie.isDetached()).toBe(false);\n      expect(otherMovie.isDetached()).toBe(false);\n    });\n  });\n\n  describe('Execution mode', () => {\n    test('getExecutionMode() and setExecutionMode()', async () => {\n      class Movie extends Component {}\n\n      class App extends Component {\n        @provide() static Movie = Movie;\n      }\n\n      expect(App.getExecutionMode()).toBe('foreground');\n      expect(Movie.getExecutionMode()).toBe('foreground');\n\n      App.setExecutionMode('background');\n\n      expect(App.getExecutionMode()).toBe('background');\n      expect(Movie.getExecutionMode()).toBe('background');\n\n      expect(() => {\n        App.setExecutionMode('foreground');\n      }).toThrow();\n    });\n  });\n\n  describe('Introspection', () => {\n    test('introspect()', async () => {\n      class Movie extends EmbeddedComponent {\n        @consume() static Cinema: typeof Cinema;\n\n        @attribute('number') static limit = 100;\n        @attribute('number?') static offset?: number;\n        @method() static find() {}\n\n        @attribute('string') title = '';\n        @attribute('string?') country?: string;\n        @method() load() {}\n      }\n\n      class Cinema extends Component {\n        @provide() static Movie = Movie;\n\n        @attribute('Movie[]?') movies?: Movie[];\n      }\n\n      const defaultTitle = Movie.prototype.getAttribute('title').getDefault();\n\n      expect(typeof defaultTitle).toBe('function');\n\n      expect(Movie.introspect()).toBeUndefined();\n\n      expect(Cinema.introspect()).toBeUndefined();\n\n      Cinema.prototype.getAttribute('movies').setExposure({get: true});\n\n      expect(Cinema.introspect()).toStrictEqual({\n        name: 'Cinema',\n        prototype: {\n          properties: [\n            {name: 'movies', type: 'Attribute', valueType: 'Movie[]?', exposure: {get: true}}\n          ]\n        }\n      });\n\n      Movie.getAttribute('limit').setExposure({get: true});\n\n      expect(Movie.introspect()).toStrictEqual({\n        name: 'Movie',\n        isEmbedded: true,\n        properties: [\n          {name: 'limit', type: 'Attribute', valueType: 'number', value: 100, exposure: {get: true}}\n        ],\n        consumedComponents: ['Cinema']\n      });\n\n      expect(Cinema.introspect()).toStrictEqual({\n        name: 'Cinema',\n        prototype: {\n          properties: [\n            {name: 'movies', type: 'Attribute', valueType: 'Movie[]?', exposure: {get: true}}\n          ]\n        },\n        providedComponents: [\n          {\n            name: 'Movie',\n            isEmbedded: true,\n            properties: [\n              {\n                name: 'limit',\n                type: 'Attribute',\n                valueType: 'number',\n                value: 100,\n                exposure: {get: true}\n              }\n            ],\n            consumedComponents: ['Cinema']\n          }\n        ]\n      });\n\n      Movie.getAttribute('limit').setExposure({get: true});\n      Movie.getAttribute('offset').setExposure({get: true});\n      Movie.getMethod('find').setExposure({call: true});\n\n      expect(Movie.introspect()).toStrictEqual({\n        name: 'Movie',\n        isEmbedded: true,\n        properties: [\n          {\n            name: 'limit',\n            type: 'Attribute',\n            valueType: 'number',\n            value: 100,\n            exposure: {get: true}\n          },\n          {\n            name: 'offset',\n            type: 'Attribute',\n            valueType: 'number?',\n            value: undefined,\n            exposure: {get: true}\n          },\n          {name: 'find', type: 'Method', exposure: {call: true}}\n        ],\n        consumedComponents: ['Cinema']\n      });\n\n      Movie.prototype.getAttribute('title').setExposure({get: true, set: true});\n      Movie.prototype.getAttribute('country').setExposure({get: true});\n      Movie.prototype.getMethod('load').setExposure({call: true});\n\n      expect(Movie.introspect()).toStrictEqual({\n        name: 'Movie',\n        isEmbedded: true,\n        properties: [\n          {\n            name: 'limit',\n            type: 'Attribute',\n            valueType: 'number',\n            value: 100,\n            exposure: {get: true}\n          },\n          {\n            name: 'offset',\n            type: 'Attribute',\n            valueType: 'number?',\n            value: undefined,\n            exposure: {get: true}\n          },\n          {name: 'find', type: 'Method', exposure: {call: true}}\n        ],\n        prototype: {\n          properties: [\n            {\n              name: 'title',\n              type: 'Attribute',\n              valueType: 'string',\n              default: defaultTitle,\n              exposure: {get: true, set: true}\n            },\n            {name: 'country', type: 'Attribute', valueType: 'string?', exposure: {get: true}},\n            {name: 'load', type: 'Method', exposure: {call: true}}\n          ]\n        },\n        consumedComponents: ['Cinema']\n      });\n\n      // --- With a mixin ---\n\n      const Storable = (Base = Component) => {\n        const _Storable = class extends Base {};\n\n        Object.defineProperty(_Storable, '__mixin', {value: 'Storable'});\n\n        return _Storable;\n      };\n\n      class Film extends Storable(Component) {\n        @expose({get: true, set: true}) @attribute('string?') title?: string;\n      }\n\n      expect(Film.introspect()).toStrictEqual({\n        name: 'Film',\n        mixins: ['Storable'],\n        prototype: {\n          properties: [\n            {\n              name: 'title',\n              type: 'Attribute',\n              valueType: 'string?',\n              exposure: {get: true, set: true}\n            }\n          ]\n        }\n      });\n    });\n\n    test('unintrospect()', async () => {\n      const defaultTitle = function () {\n        return '';\n      };\n\n      const Cinema = Component.unintrospect({\n        name: 'Cinema',\n        prototype: {\n          properties: [\n            {name: 'movies', type: 'Attribute', valueType: 'Movie[]?', exposure: {get: true}}\n          ]\n        },\n        providedComponents: [\n          {\n            name: 'Movie',\n            isEmbedded: true,\n            properties: [\n              {\n                name: 'limit',\n                type: 'Attribute',\n                valueType: 'number',\n                value: 100,\n                exposure: {get: true, set: true}\n              },\n              {name: 'find', type: 'Method', exposure: {call: true}}\n            ],\n            prototype: {\n              properties: [\n                {\n                  name: 'title',\n                  type: 'Attribute',\n                  valueType: 'string',\n                  default: defaultTitle,\n                  exposure: {get: true, set: true}\n                }\n              ]\n            },\n            consumedComponents: ['Cinema']\n          }\n        ]\n      });\n\n      expect(Cinema.getComponentName()).toBe('Cinema');\n      expect(Cinema.isEmbedded()).toBe(false);\n      expect(Cinema.prototype.getAttribute('movies').getValueType().toString()).toBe('Movie[]?');\n      expect(Cinema.prototype.getAttribute('movies').getExposure()).toStrictEqual({get: true});\n      expect(Cinema.prototype.getAttribute('movies').isControlled()).toBe(true);\n\n      const Movie = Cinema.getProvidedComponent('Movie')!;\n\n      expect(Movie.getComponentName()).toBe('Movie');\n      expect(Movie.isEmbedded()).toBe(true);\n      expect(Movie.getAttribute('limit').getValue()).toBe(100);\n      expect(Movie.getAttribute('limit').getExposure()).toStrictEqual({get: true, set: true});\n      expect(Movie.getAttribute('limit').isControlled()).toBe(false);\n      expect(Movie.getMethod('find').getExposure()).toStrictEqual({call: true});\n      expect(Movie.getConsumedComponent('Cinema')).toBe(Cinema);\n\n      expect(Movie.prototype.getAttribute('title').getDefault()).toBe(defaultTitle);\n      expect(Movie.prototype.getAttribute('title').getExposure()).toStrictEqual({\n        get: true,\n        set: true\n      });\n      expect(Movie.prototype.getAttribute('title').isControlled()).toBe(false);\n\n      const movie = new Movie();\n\n      expect(movie.getAttribute('title').getValue()).toBe('');\n\n      // --- With a mixin ---\n\n      const Storable = (Base = Component) => {\n        const _Storable = class extends Base {\n          static storableMethod() {}\n        };\n\n        Object.defineProperty(_Storable, '__mixin', {value: 'Storable'});\n\n        return _Storable;\n      };\n\n      const Film = Component.unintrospect(\n        {\n          name: 'Film',\n          mixins: ['Storable'],\n          prototype: {\n            properties: [\n              {\n                name: 'title',\n                type: 'Attribute',\n                valueType: 'string?',\n                exposure: {get: true, set: true}\n              }\n            ]\n          }\n        },\n        {mixins: [Storable]}\n      );\n\n      expect(Film.getComponentName()).toBe('Film');\n      expect(Film.prototype.getAttribute('title').getValueType().toString()).toBe('string?');\n      expect(typeof (Film as any).storableMethod).toBe('function');\n\n      expect(() => Component.unintrospect({name: 'Film', mixins: ['Storable']})).toThrow(\n        \"Couldn't find a component mixin named 'Storable'. Please make sure you specified it when creating your 'ComponentClient'.\"\n      );\n    });\n  });\n\n  describe('Utilities', () => {\n    test('toObject()', async () => {\n      class MovieDetails extends EmbeddedComponent {\n        @attribute() duration = 0;\n      }\n\n      class Movie extends Component {\n        @provide() static MovieDetails = MovieDetails;\n\n        @attribute() title = '';\n        @attribute('MovieDetails') details!: MovieDetails;\n      }\n\n      const movie = new Movie({title: 'Inception', details: new MovieDetails({duration: 120})});\n\n      expect(movie.toObject()).toStrictEqual({title: 'Inception', details: {duration: 120}});\n\n      class Cinema extends Component {\n        @provide() static Movie = Movie;\n\n        @attribute() name = '';\n        @attribute('Movie[]') movies = new Array<Movie>();\n      }\n\n      const cinema = new Cinema({name: 'Paradiso', movies: [movie]});\n\n      expect(cinema.toObject()).toStrictEqual({\n        name: 'Paradiso',\n        movies: [{title: 'Inception', details: {duration: 120}}]\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/component/src/component.ts",
    "content": "import {Observable} from '@layr/observable';\nimport {throwError} from '@layr/utilities';\nimport {\n  hasOwnProperty,\n  isPrototypeOf,\n  getTypeOf,\n  PlainObject,\n  isPlainObject,\n  PromiseLikeable,\n  getFunctionName,\n  assertIsFunction\n} from 'core-helpers';\nimport {possiblyAsync} from 'possibly-async';\nimport cuid from 'cuid';\nimport isEmpty from 'lodash/isEmpty';\n\nimport {\n  Property,\n  PropertyOptions,\n  PropertyOperationSetting,\n  PropertyFilterSync,\n  IntrospectedProperty,\n  Attribute,\n  isAttributeClass,\n  isAttributeInstance,\n  AttributeOptions,\n  ValueSource,\n  IntrospectedAttribute,\n  IdentifierAttribute,\n  isIdentifierAttributeInstance,\n  PrimaryIdentifierAttribute,\n  isPrimaryIdentifierAttributeInstance,\n  SecondaryIdentifierAttribute,\n  isSecondaryIdentifierAttributeInstance,\n  IdentifierValue,\n  AttributeSelector,\n  getFromAttributeSelector,\n  setWithinAttributeSelector,\n  normalizeAttributeSelector,\n  Method,\n  isMethodInstance,\n  MethodOptions,\n  IntrospectedMethod,\n  isComponentValueTypeInstance\n} from './properties';\nimport {IdentityMap} from './identity-map';\nimport {clone, CloneOptions} from './cloning';\nimport {ForkOptions} from './forking';\nimport {merge, MergeOptions} from './merging';\nimport {SerializeOptions} from './serialization';\nimport {DeserializeOptions} from './deserialization';\nimport {\n  isComponentClass,\n  isComponentInstance,\n  isComponentClassOrInstance,\n  assertIsComponentClass,\n  assertIsComponentInstance,\n  ensureComponentClass,\n  assertIsComponentName,\n  getComponentNameFromComponentClassType,\n  getComponentNameFromComponentInstanceType,\n  assertIsComponentType,\n  getComponentClassTypeFromComponentName,\n  getComponentInstanceTypeFromComponentName,\n  joinAttributePath\n} from './utilities';\n\nexport type ComponentSet = Set<typeof Component | Component>;\n\nexport type ComponentMixin = (Base: typeof Component) => typeof Component;\n\nexport type TraverseAttributesIteratee = (attribute: Attribute) => void;\n\nexport type TraverseAttributesOptions = {\n  attributeSelector: AttributeSelector;\n  setAttributesOnly: boolean;\n};\n\nexport type IdentifierDescriptor = NormalizedIdentifierDescriptor | IdentifierValue;\n\nexport type NormalizedIdentifierDescriptor = {[name: string]: IdentifierValue};\n\nexport type IdentifierSelector = NormalizedIdentifierSelector | IdentifierValue;\n\nexport type NormalizedIdentifierSelector = {[name: string]: IdentifierValue};\n\nexport type ResolveAttributeSelectorOptions = {\n  filter?: PropertyFilterSync;\n  setAttributesOnly?: boolean;\n  target?: ValueSource;\n  aggregationMode?: 'union' | 'intersection';\n  includeReferencedComponents?: boolean;\n  alwaysIncludePrimaryIdentifierAttributes?: boolean;\n  allowPartialArrayItems?: boolean;\n  depth?: number;\n  _isDeep?: boolean;\n  _skipUnchangedAttributes?: boolean;\n  _isArrayItem?: boolean;\n  _attributeStack?: Set<Attribute>;\n};\n\ntype MethodBuilder = (name: string) => Function;\n\nexport type ExecutionMode = 'foreground' | 'background';\n\nexport type IntrospectedComponent = {\n  name: string;\n  isEmbedded?: boolean;\n  mixins?: string[];\n  properties?: (IntrospectedProperty | IntrospectedAttribute | IntrospectedMethod)[];\n  prototype?: {\n    properties?: (IntrospectedProperty | IntrospectedAttribute | IntrospectedMethod)[];\n  };\n  providedComponents?: IntrospectedComponent[];\n  consumedComponents?: string[];\n};\n\ntype IntrospectedComponentMap = Map<typeof Component, IntrospectedComponent | undefined>;\n\n/**\n * *Inherits from [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class).*\n *\n * A component is an elementary building block allowing you to define your data models and implement the business logic of your app. Typically, an app is composed of several components that are connected to each other by using the [`@provide()`](https://layrjs.com/docs/v2/reference/component#provide-decorator) and [`@consume()`](https://layrjs.com/docs/v2/reference/component#consume-decorator) decorators.\n *\n * #### Usage\n *\n * Just extend the `Component` class to define a component with some attributes and methods that are specific to your app.\n *\n * For example, a `Movie` component with a `title` attribute and a `play()` method could be defined as follows:\n *\n * ```\n * // JS\n *\n * import {Component} from '@layr/component';\n *\n * class Movie extends Component {\n *   ﹫attribute('string') title;\n *\n *   ﹫method() play() {\n *     console.log(`Playing '${this.title}...'`);\n *   }\n * }\n * ```\n *\n * ```\n * // TS\n *\n * import {Component} from '@layr/component';\n *\n * class Movie extends Component {\n *   ﹫attribute('string') title!: string;\n *\n *   ﹫method() play() {\n *     console.log(`Playing '${this.title}...'`);\n *   }\n * }\n * ```\n *\n * The [`@attribute()`](https://layrjs.com/docs/v2/reference/component#attribute-decorator) and [`@method()`](https://layrjs.com/docs/v2/reference/component#method-decorator) decorators allows you to get the full power of Layr, such as attribute validation or remote method invocation.\n *\n * > Note that you don't need to use the [`@method()`](https://layrjs.com/docs/v2/reference/component#method-decorator) decorator for all your methods. Typically, you use this decorator only for some backend methods that you want to expose to the frontend.\n *\n * Once you have defined a component, you can use it as any JavaScript/TypeScript class:\n *\n * ```\n * const movie = new Movie({title: 'Inception'});\n *\n * movie.play(); // => 'Playing Inception...'\n * ```\n *\n * #### Embedded Components\n *\n * Use the [`EmbeddedComponent`](https://layrjs.com/docs/v2/reference/embedded-component) class to embed a component into another component. An embedded component is strongly attached to the parent component that owns it, and it cannot \"live\" by itself like a regular component.\n *\n * Here are some characteristics of an embedded component:\n *\n * - An embedded component has one parent only, and therefore cannot be embedded in more than one component.\n * - When the parent of an embedded component is [validated](https://layrjs.com/docs/v2/reference/validator), the embedded component is validated as well.\n * - When the parent of an embedded component is loaded, saved, or deleted (using a [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storage-operations) method), the embedded component is loaded, saved, or deleted as well.\n *\n * See the [`EmbeddedComponent`](https://layrjs.com/docs/v2/reference/embedded-component) class for an example of use.\n *\n * #### Referenced Components\n *\n * Any non-embedded component can be referenced by another component. Contrary to an embedded component, a referenced component is an independent entity that can \"live\" by itself. So a referenced component behaves like a regular JavaScript object that can be referenced by another object.\n *\n * Here are some characteristics of a referenced component:\n *\n * - A referenced component can be referenced by any number of components.\n * - When a component holding a reference to another component is [validated](https://layrjs.com/docs/v2/reference/validator), the referenced component is considered as an independent entity, and is therefore not validated automatically.\n * - When a component holding a reference to another component is loaded, saved, or deleted (using a [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storage-operations) method), the referenced component may optionally be loaded in the same operation, but it has to be saved or deleted independently.\n *\n * For example, let's say we have a `Director` component defined as follows:\n *\n * ```\n * // JS\n *\n * class Director extends Component {\n *   ﹫attribute('string') fullName;\n * }\n * ```\n *\n * ```\n * // TS\n *\n * class Director extends Component {\n *   ﹫attribute('string') fullName!: string;\n * }\n * ```\n *\n * Next, we can add an attribute to the `Movie` component to store a reference to a `Director`:\n *\n * ```\n * // JS\n *\n * class Movie extends Component {\n *   ﹫provide() static Director = Director;\n *\n *   // ...\n *\n *   ﹫attribute('Director') director;\n * }\n * ```\n *\n * ```\n * // TS\n *\n * class Movie extends Component {\n *   ﹫provide() static Director = Director;\n *\n *   // ...\n *\n *   ﹫attribute('Director') director!: Director;\n * }\n * ```\n *\n * > Note that to be able to specify the `'Director'` type for the `director` attribute, you first have to make the `Movie` component aware of the `Director` component by using the [`@provide()`](https://layrjs.com/docs/v2/reference/component#provide-decorator) decorator.\n *\n *  Then, to create a `Movie` with a `Director`, we can do something like the following:\n *\n * ```\n * const movie = new Movie({\n *   title: 'Inception',\n *   director: new Director({fullName: 'Christopher Nolan'})\n * });\n *\n * movie.title; // => 'Inception'\n * movie.director.fullName; // => 'Christopher Nolan'\n * ```\n */\nexport class Component extends Observable(Object) {\n  declare ['constructor']: typeof Component;\n\n  // === Creation ===\n\n  /**\n   * Creates an instance of a component class.\n   *\n   * @param [object] An optional object specifying the value of the component attributes.\n   *\n   * @returns The component instance that was created.\n   *\n   * @example\n   * ```\n   * // JS\n   *\n   * import {Component, attribute} from '﹫layr/component';\n   *\n   * class Movie extends Component {\n   *   ﹫attribute('string') title;\n   * }\n   *\n   * const movie = new Movie({title: 'Inception'});\n   *\n   * movie.title // => 'Inception'\n   * ```\n   *\n   * @example\n   * ```\n   * // TS\n   *\n   * import {Component, attribute} from '﹫layr/component';\n   *\n   * class Movie extends Component {\n   *   ﹫attribute('string') title!: string;\n   * }\n   *\n   * const movie = new Movie({title: 'Inception'});\n   *\n   * movie.title // => 'Inception'\n   * ```\n   *\n   * @category Creation\n   */\n  constructor(object: PlainObject = {}) {\n    super();\n\n    this.markAsNew();\n\n    for (const attribute of this.getAttributes()) {\n      const name = attribute.getName();\n      let value;\n\n      if (hasOwnProperty(object, name)) {\n        value = object[name];\n      } else {\n        if (attribute.isControlled()) {\n          continue; // Controlled attributes should not be set\n        }\n\n        value = attribute.evaluateDefault();\n      }\n\n      attribute.setValue(value);\n      attribute._fixDecoration();\n    }\n  }\n\n  static instantiate<T extends typeof Component>(\n    this: T,\n    identifiers?: IdentifierSelector,\n    options: {\n      source?: ValueSource;\n    } = {}\n  ): InstanceType<T> {\n    const {source} = options;\n\n    let component: InstanceType<T> | undefined;\n\n    if (this.prototype.hasPrimaryIdentifierAttribute()) {\n      if (identifiers === undefined) {\n        throw new Error(\n          `An identifier is required to instantiate an identifiable component, but received a value of type '${getTypeOf(\n            identifiers\n          )}' (${this.describeComponent()})`\n        );\n      }\n\n      identifiers = this.normalizeIdentifierSelector(identifiers);\n\n      component = this.getIdentityMap().getComponent(identifiers) as InstanceType<T> | undefined;\n\n      if (component === undefined) {\n        component = Object.create(this.prototype);\n\n        for (const [name, value] of Object.entries(identifiers)) {\n          component!.getAttribute(name).setValue(value, {source});\n        }\n      }\n    } else {\n      // The component does not have an identifier attribute\n\n      if (!(identifiers === undefined || (isPlainObject(identifiers) && isEmpty(identifiers)))) {\n        throw new Error(\n          `Cannot instantiate an unidentifiable component with an identifier (${this.describeComponent()})`\n        );\n      }\n\n      component = Object.create(this.prototype);\n    }\n\n    return component!;\n  }\n\n  // === Initialization ===\n\n  /**\n   * Invoke this method to call any `initializer()` static method defined in your component classes.\n   *\n   * If the current class has an `initializer()` static method, it is invoked, and if the current class has some other components as dependencies, the `initializer()` method is also invoked for those components.\n   *\n   * Note that your `initializer()` methods can be asynchronous, and therefore you should call the `initialize()` method with `await`.\n   *\n   * Typically, you will call the `initialize()` method on the root component of your frontend service when your app starts. Backend services are usually managed by a [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server), which automatically invokes the `initialize()` method on the root component.\n   *\n   * Note that if you use [Boostr](https://boostr.dev) to manage your frontend service, you should not call the `initialize()` method manually.\n   *\n   * @category Initialization\n   * @possiblyasync\n   */\n  static initialize() {\n    return possiblyAsync.forEach(this.traverseComponents(), (component) => {\n      const initializer = (component as any).initializer;\n\n      if (typeof initializer === 'function') {\n        return initializer.call(component);\n      }\n    });\n  }\n\n  // === Naming ===\n\n  static getBaseComponentName() {\n    return 'Component';\n  }\n\n  /**\n   * Returns the name of the component, which is usually the name of the corresponding class.\n   *\n   * @returns A string.\n   *\n   * @example\n   * ```\n   * Movie.getComponentName(); // => 'Movie'\n   * ```\n   *\n   * @category Naming\n   */\n  static getComponentName() {\n    const name = this.name;\n\n    if (typeof name === 'string' && name !== '') {\n      return name;\n    }\n\n    throw new Error('The name of the component is missing');\n  }\n\n  /**\n   * Sets the name of the component. As the name of a component is usually inferred from the name of its class, this method should rarely be used.\n   *\n   * @param name The name you wish for the component.\n   *\n   * @example\n   * ```\n   * Movie.getComponentName(); // => 'Movie'\n   * Movie.setComponentName('Film');\n   * Movie.getComponentName(); // => 'Film'\n   * ```\n   *\n   * @category Naming\n   */\n  static setComponentName(name: string) {\n    assertIsComponentName(name);\n\n    Object.defineProperty(this, 'name', {value: name});\n  }\n\n  /**\n   * Returns the path of the component starting from its root component.\n   *\n   * For example, if an `Application` component provides a `Movie` component, this method will return `'Application.Movie'` when called on the `Movie` component.\n   *\n   * @returns A string.\n   *\n   * @example\n   * ```\n   * class Movie extends Component {}\n   *\n   * Movie.getComponentPath(); // => 'Movie'\n   *\n   * class Application extends Component {\n   *   ﹫provide() static Movie = Movie;\n   * }\n   *\n   * Movie.getComponentPath(); // => 'Application.Movie'\n   * ```\n   *\n   * @category Naming\n   */\n  static getComponentPath() {\n    let path: string[] = [];\n    let currentComponent = this;\n\n    while (true) {\n      path.unshift(currentComponent.getComponentName());\n\n      const componentProvider = currentComponent.getComponentProvider();\n\n      if (componentProvider === currentComponent) {\n        break;\n      }\n\n      currentComponent = componentProvider;\n    }\n\n    return path.join('.');\n  }\n\n  // === Typing ===\n\n  static getBaseComponentType() {\n    return getComponentClassTypeFromComponentName(this.getBaseComponentName());\n  }\n\n  getBaseComponentType() {\n    return getComponentInstanceTypeFromComponentName(this.constructor.getBaseComponentName());\n  }\n\n  /**\n   * Returns the type of the component class. A component class type is composed of the component class name prefixed with the string `'typeof '`.\n   *\n   * For example, with a component class named `'Movie'`, this method will return `'typeof Movie'`.\n   *\n   * @returns A string.\n   *\n   * @example\n   * ```\n   * Movie.getComponentType(); // => 'typeof Movie'\n   * ```\n   *\n   * @category Typing\n   */\n  static getComponentType() {\n    return getComponentClassTypeFromComponentName(this.getComponentName());\n  }\n\n  /**\n   * Returns the type of the component instance. A component instance type is equivalent to the component class name.\n   *\n   * For example, with a component class named `'Movie'`, this method will return `'Movie'` when called on a `Movie` instance.\n   *\n   * @returns A string.\n   *\n   * @example\n   * ```\n   * Movie.prototype.getComponentType(); // => 'Movie'\n   *\n   * const movie = new Movie();\n   * movie.getComponentType(); // => 'Movie'\n   * ```\n   *\n   * @category Typing\n   */\n  getComponentType() {\n    return getComponentInstanceTypeFromComponentName(this.constructor.getComponentName());\n  }\n\n  // === isNew Mark ===\n\n  __isNew: boolean | undefined;\n\n  /**\n   * Returns whether the component instance is marked as new or not.\n   *\n   * @alias isNew\n   *\n   * @returns A boolean.\n   *\n   * @example\n   * ```\n   * let movie = new Movie();\n   * movie.getIsNewMark(); // => true\n   * ```\n   *\n   * @category isNew Mark\n   */\n  getIsNewMark() {\n    return this.__isNew === true;\n  }\n\n  /**\n   * Sets whether the component instance is marked as new or not.\n   *\n   * @param isNew A boolean specifying if the component instance should be marked as new or not.\n   *\n   * @example\n   * ```\n   * const movie = new Movie();\n   * movie.getIsNewMark(); // => true\n   * movie.setIsNewMark(false);\n   * movie.getIsNewMark(); // => false\n   * ```\n   *\n   * @category isNew Mark\n   */\n  setIsNewMark(isNew: boolean) {\n    Object.defineProperty(this, '__isNew', {value: isNew, configurable: true});\n  }\n\n  /**\n   * Returns whether the component instance is marked as new or not.\n   *\n   * @returns A boolean.\n   *\n   * @example\n   * ```\n   * let movie = new Movie();\n   * movie.isNew(); // => true\n   * ```\n   *\n   * @category isNew Mark\n   */\n  isNew() {\n    return this.getIsNewMark();\n  }\n\n  /**\n   * Marks the component instance as new.\n   *\n   * This method is a shortcut for `setIsNewMark(true)`.\n   *\n   * @category isNew Mark\n   */\n  markAsNew() {\n    this.setIsNewMark(true);\n  }\n\n  /**\n   * Marks the component instance as not new.\n   *\n   * This method is a shortcut for `setIsNewMark(false)`.\n   *\n   * @category isNew Mark\n   */\n  markAsNotNew() {\n    this.setIsNewMark(false);\n  }\n\n  // === Observability ===\n\n  /**\n   * See the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) class.\n   *\n   * @category Observability\n   */\n\n  // === Embeddability ===\n\n  /**\n   * Returns whether the component is an [`EmbeddedComponent`](https://layrjs.com/docs/v2/reference/embedded-component).\n   *\n   * @returns A boolean.\n   *\n   * @category Embeddability\n   */\n  static isEmbedded() {\n    return false;\n  }\n\n  // === Properties ===\n\n  static getPropertyClass(type: string) {\n    if (type === 'Property') {\n      return Property;\n    }\n\n    if (type === 'Attribute') {\n      return Attribute;\n    }\n\n    if (type === 'PrimaryIdentifierAttribute') {\n      return PrimaryIdentifierAttribute;\n    }\n\n    if (type === 'SecondaryIdentifierAttribute') {\n      return SecondaryIdentifierAttribute;\n    }\n\n    if (type === 'Method') {\n      return Method;\n    }\n\n    throw new Error(`The specified property type ('${type}') is unknown`);\n  }\n\n  /**\n   * Gets a property of the component.\n   *\n   * @param name The name of the property to get.\n   *\n   * @returns An instance of a [`Property`](https://layrjs.com/docs/v2/reference/property) (or a subclass of [`Property`](https://layrjs.com/docs/v2/reference/property) such as [`Attribute`](https://layrjs.com/docs/v2/reference/attribute), [`Method`](https://layrjs.com/docs/v2/reference/method), etc.).\n   *\n   * @example\n   * ```\n   * movie.getProperty('title'); // => 'title' attribute property\n   * movie.getProperty('play'); // => 'play()' method property\n   * ```\n   *\n   * @category Properties\n   */\n  static get getProperty() {\n    return this.prototype.getProperty;\n  }\n\n  /**\n   * Gets a property of the component.\n   *\n   * @param name The name of the property to get.\n   *\n   * @returns An instance of a [`Property`](https://layrjs.com/docs/v2/reference/property) (or a subclass of [`Property`](https://layrjs.com/docs/v2/reference/property) such as [`Attribute`](https://layrjs.com/docs/v2/reference/attribute), [`Method`](https://layrjs.com/docs/v2/reference/method), etc.).\n   *\n   * @example\n   * ```\n   * movie.getProperty('title'); // => 'title' attribute property\n   * movie.getProperty('play'); // => 'play()' method property\n   * ```\n   *\n   * @category Properties\n   */\n  getProperty(name: string, options: {autoFork?: boolean} = {}) {\n    const {autoFork = true} = options;\n\n    const property = this.__getProperty(name, {autoFork});\n\n    if (property === undefined) {\n      throw new Error(`The property '${name}' is missing (${this.describeComponent()})`);\n    }\n\n    return property;\n  }\n\n  /**\n   * Returns whether the component has the specified property.\n   *\n   * @param name The name of the property to check.\n   *\n   * @returns A boolean.\n   *\n   * @example\n   * ```\n   * movie.hasProperty('title'); // => true\n   * movie.hasProperty('play'); // => true\n   * movie.hasProperty('name'); // => false\n   * ```\n   *\n   * @category Properties\n   */\n  static get hasProperty() {\n    return this.prototype.hasProperty;\n  }\n\n  /**\n   * Returns whether the component has the specified property.\n   *\n   * @param name The name of the property to check.\n   *\n   * @returns A boolean.\n   *\n   * @example\n   * ```\n   * movie.hasProperty('title'); // => true\n   * movie.hasProperty('play'); // => true\n   * movie.hasProperty('name'); // => false\n   * ```\n   *\n   * @category Properties\n   */\n  hasProperty(name: string) {\n    return this.__getProperty(name, {autoFork: false}) !== undefined;\n  }\n\n  static get __getProperty() {\n    return this.prototype.__getProperty;\n  }\n\n  __getProperty(name: string, options: {autoFork: boolean}) {\n    const {autoFork} = options;\n\n    const properties = this.__getProperties();\n\n    let property = properties[name];\n\n    if (property === undefined) {\n      return undefined;\n    }\n\n    if (autoFork && property.getParent() !== this) {\n      property = property.fork(this);\n      properties[name] = property;\n    }\n\n    return property;\n  }\n\n  /**\n   * Defines a property in the component. Typically, instead of using this method, you would rather use a decorator such as [`@attribute()`](https://layrjs.com/docs/v2/reference/component#attribute-decorator) or [`@method()`](https://layrjs.com/docs/v2/reference/component#method-decorator).\n   *\n   * @param name The name of the property to define.\n   * @param PropertyClass The class of the property (e.g., [`Attribute`](https://layrjs.com/docs/v2/reference/attribute), [`Method`](https://layrjs.com/docs/v2/reference/method)) to use.\n   * @param [propertyOptions] The options to create the `PropertyClass`.\n   *\n   * @returns The property that was created.\n   *\n   * @example\n   * ```\n   * Movie.prototype.setProperty('title', Attribute, {valueType: 'string'});\n   * ```\n   *\n   * @category Properties\n   */\n  static get setProperty() {\n    return this.prototype.setProperty;\n  }\n\n  /**\n   * Defines a property in the component. Typically, instead of using this method, you would rather use a decorator such as [`@attribute()`](https://layrjs.com/docs/v2/reference/component#attribute-decorator) or [`@method()`](https://layrjs.com/docs/v2/reference/component#method-decorator).\n   *\n   * @param name The name of the property to define.\n   * @param PropertyClass The class of the property (e.g., [`Attribute`](https://layrjs.com/docs/v2/reference/attribute), [`Method`](https://layrjs.com/docs/v2/reference/method)) to use.\n   * @param [propertyOptions] The options to create the `PropertyClass`.\n   *\n   * @returns The property that was created.\n   *\n   * @example\n   * ```\n   * Movie.prototype.setProperty('title', Attribute, {valueType: 'string'});\n   * ```\n   *\n   * @category Properties\n   */\n  setProperty<T extends typeof Property>(\n    name: string,\n    PropertyClass: T,\n    propertyOptions?: PropertyOptions\n  ): InstanceType<T>;\n  setProperty(name: string, PropertyClass: typeof Property, propertyOptions: PropertyOptions = {}) {\n    let property = this.hasProperty(name) ? this.getProperty(name) : undefined;\n\n    if (property === undefined) {\n      property = new PropertyClass(name, this, propertyOptions);\n      const properties = this.__getProperties();\n      properties[name] = property;\n    } else {\n      if (getTypeOf(property) !== getTypeOf(PropertyClass.prototype)) {\n        throw new Error(`Cannot change the type of a property (${property.describe()})`);\n      }\n      property.setOptions(propertyOptions);\n    }\n\n    if (isAttributeClass(PropertyClass)) {\n      const descriptor: PropertyDescriptor = {\n        configurable: true,\n        enumerable: true,\n        get(this: typeof Component | Component) {\n          return this.getAttribute(name).getValue();\n        },\n        set(this: typeof Component | Component, value: any): void {\n          this.getAttribute(name).setValue(value);\n        }\n      };\n\n      Object.defineProperty(this, name, descriptor);\n    }\n\n    return property;\n  }\n\n  /**\n   * Removes a property from the component. If the specified property doesn't exist, nothing happens.\n   *\n   * @param name The name of the property to remove.\n   *\n   * @returns A boolean.\n   *\n   * @category Properties\n   */\n  static get deleteProperty() {\n    return this.prototype.deleteProperty;\n  }\n\n  /**\n   * Removes a property from the component. If the specified property doesn't exist, nothing happens.\n   *\n   * @param name The name of the property to remove.\n   *\n   * @returns A boolean.\n   *\n   * @category Properties\n   */\n  deleteProperty(name: string) {\n    const properties = this.__getProperties();\n\n    if (!hasOwnProperty(properties, name)) {\n      return false;\n    }\n\n    delete properties[name];\n\n    return true;\n  }\n\n  /**\n   * Returns an iterator providing the properties of the component.\n   *\n   * @param [options.filter] A function used to filter the properties to be returned. The function is invoked for each property with a [`Property`](https://layrjs.com/docs/v2/reference/property) instance as first argument.\n   * @param [options.attributesOnly] A boolean specifying whether only attribute properties should be returned (default: `false`).\n   * @param [options.setAttributesOnly] A boolean specifying whether only set attributes should be returned (default: `false`).\n   * @param [options.attributeSelector] An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be returned (default: `true`, which means that all the attributes should be returned).\n   * @param [options.methodsOnly] A boolean specifying whether only method properties should be returned (default: `false`).\n   *\n   * @returns A [`Property`](https://layrjs.com/docs/v2/reference/property) instance iterator.\n   *\n   * @example\n   * ```\n   * for (const property of movie.getProperties()) {\n   *   console.log(property.getName());\n   * }\n   *\n   * // Should output:\n   * // title\n   * // play\n   * ```\n   *\n   * @category Properties\n   */\n  static get getProperties() {\n    return this.prototype.getProperties;\n  }\n\n  /**\n   * Returns an iterator providing the properties of the component.\n   *\n   * @param [options.filter] A function used to filter the properties to be returned. The function is invoked for each property with a [`Property`](https://layrjs.com/docs/v2/reference/property) instance as first argument.\n   * @param [options.attributesOnly] A boolean specifying whether only attribute properties should be returned (default: `false`).\n   * @param [options.setAttributesOnly] A boolean specifying whether only set attributes should be returned (default: `false`).\n   * @param [options.attributeSelector] An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be returned (default: `true`, which means that all the attributes should be returned).\n   * @param [options.methodsOnly] A boolean specifying whether only method properties should be returned (default: `false`).\n   *\n   * @returns A [`Property`](https://layrjs.com/docs/v2/reference/property) instance iterator.\n   *\n   * @example\n   * ```\n   * for (const property of movie.getProperties()) {\n   *   console.log(property.getName());\n   * }\n   *\n   * // Should output:\n   * // title\n   * // play\n   * ```\n   *\n   * @category Properties\n   */\n  getProperties<PropertyType extends Property = Property>(\n    options: {\n      filter?: PropertyFilterSync;\n      autoFork?: boolean;\n    } & CreatePropertyFilterOptions = {}\n  ) {\n    const {\n      filter: originalFilter,\n      autoFork = true,\n      attributesOnly = false,\n      attributeSelector = true,\n      setAttributesOnly = false,\n      methodsOnly = false\n    } = options;\n\n    const component = this;\n\n    const filter = createPropertyFilter(originalFilter, {\n      attributesOnly,\n      attributeSelector,\n      setAttributesOnly,\n      methodsOnly\n    });\n\n    return {\n      *[Symbol.iterator]() {\n        for (const name of component.getPropertyNames()) {\n          const property = component.getProperty(name, {autoFork});\n\n          if (filter.call(component, property)) {\n            yield property as PropertyType;\n          }\n        }\n      }\n    };\n  }\n\n  static __properties?: {[name: string]: Property};\n\n  __properties?: {[name: string]: Property};\n\n  /**\n   * Returns the name of all the properties of the component.\n   *\n   * @returns An array of the property names.\n   *\n   * @example\n   * ```\n   * movie.getPropertyNames(); // => ['title', 'play']\n   * ```\n   *\n   * @category Properties\n   */\n  static get getPropertyNames() {\n    return this.prototype.getPropertyNames;\n  }\n\n  /**\n   * Returns the name of all the properties of the component.\n   *\n   * @returns An array of the property names.\n   *\n   * @example\n   * ```\n   * movie.getPropertyNames(); // => ['title', 'play']\n   * ```\n   *\n   * @category Properties\n   */\n  getPropertyNames() {\n    const names = [];\n\n    let currentObject: {__properties: any} = this as unknown as {__properties: any};\n    while ('__properties' in currentObject) {\n      if (hasOwnProperty(currentObject, '__properties')) {\n        const currentNames = Object.getOwnPropertyNames(currentObject.__properties);\n        names.unshift(...currentNames);\n      }\n      currentObject = Object.getPrototypeOf(currentObject);\n    }\n\n    return Array.from(new Set(names));\n  }\n\n  static get __getProperties() {\n    return this.prototype.__getProperties;\n  }\n\n  __getProperties({autoCreateOrFork = true} = {}) {\n    if (autoCreateOrFork) {\n      if (!('__properties' in this)) {\n        Object.defineProperty(this, '__properties', {value: Object.create(null)});\n      } else if (!hasOwnProperty(this, '__properties')) {\n        Object.defineProperty(this, '__properties', {value: Object.create(this.__properties!)});\n      }\n    }\n\n    return this.__properties!;\n  }\n\n  // === Property exposure ===\n\n  static normalizePropertyOperationSetting(\n    setting: PropertyOperationSetting,\n    options: {throwIfInvalid?: boolean} = {}\n  ): PropertyOperationSetting | undefined {\n    const {throwIfInvalid = true} = options;\n\n    if (setting === true) {\n      return true;\n    }\n\n    if (throwIfInvalid) {\n      throw new Error(\n        `The specified property operation setting (${JSON.stringify(setting)}) is invalid`\n      );\n    }\n\n    return undefined;\n  }\n\n  static get resolvePropertyOperationSetting() {\n    return this.prototype.resolvePropertyOperationSetting;\n  }\n\n  resolvePropertyOperationSetting(\n    setting: PropertyOperationSetting\n  ): PromiseLikeable<boolean | undefined> {\n    if (setting === true) {\n      return true;\n    }\n\n    return undefined;\n  }\n\n  // === Attribute Properties ===\n\n  __constructorSourceCode?: string; // Used by @attribute() decorator\n\n  /**\n   * Gets an attribute of the component.\n   *\n   * @param name The name of the attribute to get.\n   *\n   * @returns An instance of [`Attribute`](https://layrjs.com/docs/v2/reference/attribute).\n   *\n   * @example\n   * ```\n   * movie.getAttribute('title'); // => 'title' attribute property\n   * movie.getAttribute('play'); // => Error ('play' is a method)\n   * ```\n   *\n   * @category Attribute Properties\n   */\n  static get getAttribute() {\n    return this.prototype.getAttribute;\n  }\n\n  /**\n   * Gets an attribute of the component.\n   *\n   * @param name The name of the attribute to get.\n   *\n   * @returns An instance of [`Attribute`](https://layrjs.com/docs/v2/reference/attribute).\n   *\n   * @example\n   * ```\n   * movie.getAttribute('title'); // => 'title' attribute property\n   * movie.getAttribute('play'); // => Error ('play' is a method)\n   * ```\n   *\n   * @category Attribute Properties\n   */\n  getAttribute(name: string, options: {autoFork?: boolean} = {}) {\n    const {autoFork = true} = options;\n\n    const attribute = this.__getAttribute(name, {autoFork});\n\n    if (attribute === undefined) {\n      throw new Error(`The attribute '${name}' is missing (${this.describeComponent()})`);\n    }\n\n    return attribute;\n  }\n\n  /**\n   * Returns whether the component has the specified attribute.\n   *\n   * @param name The name of the attribute to check.\n   *\n   * @returns A boolean.\n   *\n   * @example\n   * ```\n   * movie.hasAttribute('title'); // => true\n   * movie.hasAttribute('name'); // => false\n   * movie.hasAttribute('play'); // => Error ('play' is a method)\n   * ```\n   *\n   * @category Attribute Properties\n   */\n  static get hasAttribute() {\n    return this.prototype.hasAttribute;\n  }\n\n  /**\n   * Returns whether the component has the specified attribute.\n   *\n   * @param name The name of the attribute to check.\n   *\n   * @returns A boolean.\n   *\n   * @example\n   * ```\n   * movie.hasAttribute('title'); // => true\n   * movie.hasAttribute('name'); // => false\n   * movie.hasAttribute('play'); // => Error ('play' is a method)\n   * ```\n   *\n   * @category Attribute Properties\n   */\n  hasAttribute(name: string) {\n    return this.__getAttribute(name, {autoFork: false}) !== undefined;\n  }\n\n  static get __getAttribute() {\n    return this.prototype.__getAttribute;\n  }\n\n  __getAttribute(name: string, options: {autoFork: boolean}) {\n    const {autoFork} = options;\n\n    const property = this.__getProperty(name, {autoFork});\n\n    if (property === undefined) {\n      return undefined;\n    }\n\n    if (!isAttributeInstance(property)) {\n      throw new Error(\n        `A property with the specified name was found, but it is not an attribute (${property.describe()})`\n      );\n    }\n\n    return property;\n  }\n\n  /**\n   * Defines an attribute in the component. Typically, instead of using this method, you would rather use the [`@attribute()`](https://layrjs.com/docs/v2/reference/component#attribute-decorator) decorator.\n   *\n   * @param name The name of the attribute to define.\n   * @param [attributeOptions] The options to create the [`Attribute`](https://layrjs.com/docs/v2/reference/attribute#constructor).\n   *\n   * @returns The [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) that was created.\n   *\n   * @example\n   * ```\n   * Movie.prototype.setAttribute('title', {valueType: 'string'});\n   * ```\n   *\n   * @category Attribute Properties\n   */\n  static get setAttribute() {\n    return this.prototype.setAttribute;\n  }\n\n  /**\n   * Defines an attribute in the component. Typically, instead of using this method, you would rather use the [`@attribute()`](https://layrjs.com/docs/v2/reference/component#attribute-decorator) decorator.\n   *\n   * @param name The name of the attribute to define.\n   * @param [attributeOptions] The options to create the [`Attribute`](https://layrjs.com/docs/v2/reference/attribute#constructor).\n   *\n   * @returns The [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) that was created.\n   *\n   * @example\n   * ```\n   * Movie.prototype.setAttribute('title', {valueType: 'string'});\n   * ```\n   *\n   * @category Attribute Properties\n   */\n  setAttribute(name: string, attributeOptions: AttributeOptions = {}) {\n    return this.setProperty(name, Attribute, attributeOptions);\n  }\n\n  /**\n   * Returns an iterator providing the attributes of the component.\n   *\n   * @param [options.filter] A function used to filter the attributes to be returned. The function is invoked for each attribute with an [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) instance as first argument.\n   * @param [options.setAttributesOnly] A boolean specifying whether only set attributes should be returned (default: `false`).\n   * @param [options.attributeSelector] An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be returned (default: `true`, which means that all the attributes should be returned).\n   *\n   * @returns An [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) instance iterator.\n   *\n   * @example\n   * ```\n   * for (const attr of movie.getAttributes()) {\n   *   console.log(attr.getName());\n   * }\n   *\n   * // Should output:\n   * // title\n   * ```\n   *\n   * @category Attribute Properties\n   */\n  static get getAttributes() {\n    return this.prototype.getAttributes;\n  }\n\n  /**\n   * Returns an iterator providing the attributes of the component.\n   *\n   * @param [options.filter] A function used to filter the attributes to be returned. The function is invoked for each attribute with an [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) instance as first argument.\n   * @param [options.setAttributesOnly] A boolean specifying whether only set attributes should be returned (default: `false`).\n   * @param [options.attributeSelector] An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be returned (default: `true`, which means that all the attributes should be returned).\n   *\n   * @returns An [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) instance iterator.\n   *\n   * @example\n   * ```\n   * for (const attr of movie.getAttributes()) {\n   *   console.log(attr.getName());\n   * }\n   *\n   * // Should output:\n   * // title\n   * ```\n   *\n   * @category Attribute Properties\n   */\n  getAttributes<AttributeType extends Attribute = Attribute>(\n    options: {\n      filter?: PropertyFilterSync;\n      autoFork?: boolean;\n    } & CreatePropertyFilterOptionsForAttributes = {}\n  ) {\n    const {filter, attributeSelector = true, setAttributesOnly = false, autoFork = true} = options;\n\n    return this.getProperties<AttributeType>({\n      filter,\n      autoFork,\n      attributesOnly: true,\n      attributeSelector,\n      setAttributesOnly\n    });\n  }\n\n  static get traverseAttributes() {\n    return this.prototype.traverseAttributes;\n  }\n\n  traverseAttributes(\n    iteratee: TraverseAttributesIteratee,\n    options: Partial<TraverseAttributesOptions> & ResolveAttributeSelectorOptions = {}\n  ) {\n    assertIsFunction(iteratee);\n\n    const {\n      attributeSelector = true,\n      filter,\n      setAttributesOnly = false,\n      depth = Number.MAX_SAFE_INTEGER,\n      includeReferencedComponents = false\n    } = options;\n\n    const resolvedAttributeSelector = this.resolveAttributeSelector(attributeSelector, {\n      filter,\n      setAttributesOnly,\n      depth,\n      includeReferencedComponents\n    });\n\n    this.__traverseAttributes(iteratee, {\n      attributeSelector: resolvedAttributeSelector,\n      setAttributesOnly\n    });\n  }\n\n  static get __traverseAttributes() {\n    return this.prototype.__traverseAttributes;\n  }\n\n  __traverseAttributes(\n    iteratee: TraverseAttributesIteratee,\n    {attributeSelector, setAttributesOnly}: TraverseAttributesOptions\n  ) {\n    for (const attribute of this.getAttributes({attributeSelector})) {\n      if (setAttributesOnly && !attribute.isSet()) {\n        continue;\n      }\n\n      const name = attribute.getName();\n      const subattributeSelector = getFromAttributeSelector(attributeSelector, name);\n\n      if (subattributeSelector !== false) {\n        iteratee(attribute);\n      }\n\n      attribute._traverseAttributes(iteratee, {\n        attributeSelector: subattributeSelector,\n        setAttributesOnly\n      });\n    }\n  }\n\n  // === Identifier attributes ===\n\n  /**\n   * Gets an identifier attribute of the component.\n   *\n   * @param name The name of the identifier attribute to get.\n   *\n   * @returns An instance of [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute) or [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute).\n   *\n   * @example\n   * ```\n   * movie.getIdentifierAttribute('id'); // => 'id' primary identifier attribute\n   * movie.getIdentifierAttribute('slug'); // => 'slug' secondary identifier attribute\n   * ```\n   *\n   * @category Attribute Properties\n   */\n  getIdentifierAttribute(name: string, options: {autoFork?: boolean} = {}) {\n    const {autoFork = true} = options;\n\n    const identifierAttribute = this.__getIdentifierAttribute(name, {autoFork});\n\n    if (identifierAttribute === undefined) {\n      throw new Error(\n        `The identifier attribute '${name}' is missing (${this.describeComponent()})`\n      );\n    }\n\n    return identifierAttribute;\n  }\n\n  /**\n   * Returns whether the component has the specified identifier attribute.\n   *\n   * @param name The name of the identifier attribute to check.\n   *\n   * @returns A boolean.\n   *\n   * @example\n   * ```\n   * movie.hasIdentifierAttribute('id'); // => true\n   * movie.hasIdentifierAttribute('slug'); // => true\n   * movie.hasIdentifierAttribute('name'); // => false (the property 'name' doesn't exist)\n   * movie.hasIdentifierAttribute('title'); // => Error ('title' is not an identifier attribute)\n   * ```\n   *\n   * @category Attribute Properties\n   */\n  hasIdentifierAttribute(name: string) {\n    return this.__getIdentifierAttribute(name, {autoFork: false}) !== undefined;\n  }\n\n  __getIdentifierAttribute(name: string, options: {autoFork: boolean}) {\n    const {autoFork} = options;\n\n    const property = this.__getProperty(name, {autoFork});\n\n    if (property === undefined) {\n      return undefined;\n    }\n\n    if (!isIdentifierAttributeInstance(property)) {\n      throw new Error(\n        `A property with the specified name was found, but it is not an identifier attribute (${property.describe()})`\n      );\n    }\n\n    return property;\n  }\n\n  /**\n   * Gets the primary identifier attribute of the component.\n   *\n   * @returns An instance of [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute).\n   *\n   * @example\n   * ```\n   * movie.getPrimaryIdentifierAttribute(); // => 'id' primary identifier attribute\n   * ```\n   *\n   * @category Attribute Properties\n   */\n  getPrimaryIdentifierAttribute(options: {autoFork?: boolean} = {}) {\n    const {autoFork = true} = options;\n\n    const primaryIdentifierAttribute = this.__getPrimaryIdentifierAttribute({autoFork});\n\n    if (primaryIdentifierAttribute === undefined) {\n      throw new Error(\n        `The component '${this.constructor.getComponentName()}' doesn't have a primary identifier attribute`\n      );\n    }\n\n    return primaryIdentifierAttribute;\n  }\n\n  /**\n   * Returns whether the component as a primary identifier attribute.\n   *\n   * @returns A boolean.\n   *\n   * @example\n   * ```\n   * movie.hasPrimaryIdentifierAttribute(); // => true\n   * ```\n   *\n   * @category Attribute Properties\n   */\n  hasPrimaryIdentifierAttribute() {\n    return this.__getPrimaryIdentifierAttribute({autoFork: false}) !== undefined;\n  }\n\n  __getPrimaryIdentifierAttribute(options: {autoFork: boolean}) {\n    const {autoFork} = options;\n\n    for (const identifierAttribute of this.getIdentifierAttributes({autoFork})) {\n      if (isPrimaryIdentifierAttributeInstance(identifierAttribute)) {\n        return identifierAttribute;\n      }\n    }\n\n    return undefined;\n  }\n\n  /**\n   * Defines the primary identifier attribute of the component. Typically, instead of using this method, you would rather use the [`@primaryIdentifier()`](https://layrjs.com/docs/v2/reference/component#primary-identifier-decorator) decorator.\n   *\n   * @param name The name of the primary identifier attribute to define.\n   * @param [attributeOptions] The options to create the [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute).\n   *\n   * @returns The [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute) that was created.\n   *\n   * @example\n   * ```\n   * User.prototype.setPrimaryIdentifierAttribute('id', {\n   *   valueType: 'number',\n   *   default() {\n   *     return Math.random();\n   *   }\n   * });\n   * ```\n   *\n   * @category Attribute Properties\n   */\n  setPrimaryIdentifierAttribute(name: string, attributeOptions: AttributeOptions = {}) {\n    return this.setProperty(name, PrimaryIdentifierAttribute, attributeOptions);\n  }\n\n  /**\n   * Gets a secondary identifier attribute of the component.\n   *\n   * @param name The name of the secondary identifier attribute to get.\n   *\n   * @returns A [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute) instance.\n   *\n   * @example\n   * ```\n   * movie.getSecondaryIdentifierAttribute('slug'); // => 'slug' secondary identifier attribute\n   * movie.getSecondaryIdentifierAttribute('id'); // => Error ('id' is not a secondary identifier attribute)\n   * ```\n   *\n   * @category Attribute Properties\n   */\n  getSecondaryIdentifierAttribute(name: string, options: {autoFork?: boolean} = {}) {\n    const {autoFork = true} = options;\n\n    const secondaryIdentifierAttribute = this.__getSecondaryIdentifierAttribute(name, {autoFork});\n\n    if (secondaryIdentifierAttribute === undefined) {\n      throw new Error(\n        `The secondary identifier attribute '${name}' is missing (${this.describeComponent()})`\n      );\n    }\n\n    return secondaryIdentifierAttribute;\n  }\n\n  /**\n   * Returns whether the component has the specified secondary identifier attribute.\n   *\n   * @param name The name of the secondary identifier attribute to check.\n   *\n   * @returns A boolean.\n   *\n   * @example\n   * ```\n   * movie.hasSecondaryIdentifierAttribute('slug'); // => true\n   * movie.hasSecondaryIdentifierAttribute('name'); // => false (the property 'name' doesn't exist)\n   * movie.hasSecondaryIdentifierAttribute('id'); // => Error ('id' is not a secondary identifier attribute)\n   * ```\n   *\n   * @category Attribute Properties\n   */\n  hasSecondaryIdentifierAttribute(name: string) {\n    return this.__getSecondaryIdentifierAttribute(name, {autoFork: false}) !== undefined;\n  }\n\n  __getSecondaryIdentifierAttribute(name: string, options: {autoFork: boolean}) {\n    const {autoFork} = options;\n\n    const property = this.__getProperty(name, {autoFork});\n\n    if (property === undefined) {\n      return undefined;\n    }\n\n    if (!isSecondaryIdentifierAttributeInstance(property)) {\n      throw new Error(\n        `A property with the specified name was found, but it is not a secondary identifier attribute (${property.describe()})`\n      );\n    }\n\n    return property;\n  }\n\n  /**\n   * Defines a secondary identifier attribute in the component. Typically, instead of using this method, you would rather use the [`@secondaryIdentifier()`](https://layrjs.com/docs/v2/reference/component#secondary-identifier-decorator) decorator.\n   *\n   * @param name The name of the secondary identifier attribute to define.\n   * @param [attributeOptions] The options to create the [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute).\n   *\n   * @returns The [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute) that was created.\n   *\n   * @example\n   * ```\n   * User.prototype.setSecondaryIdentifierAttribute('slug', {valueType: 'string'});\n   * ```\n   *\n   * @category Attribute Properties\n   */\n  setSecondaryIdentifierAttribute(name: string, attributeOptions: AttributeOptions = {}) {\n    return this.setProperty(name, SecondaryIdentifierAttribute, attributeOptions);\n  }\n\n  /**\n   * Returns an iterator providing the identifier attributes of the component.\n   *\n   * @param [options.filter] A function used to filter the identifier attributes to be returned. The function is invoked for each identifier attribute with an `IdentifierAttribute` instance as first argument.\n   * @param [options.setAttributesOnly] A boolean specifying whether only set identifier attributes should be returned (default: `false`).\n   * @param [options.attributeSelector] An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the identifier attributes to be returned (default: `true`, which means that all identifier attributes should be returned).\n   *\n   * @returns An iterator of [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute) or [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute).\n   *\n   * @example\n   * ```\n   * for (const attr of movie.getIdentifierAttributes()) {\n   *   console.log(attr.getName());\n   * }\n   *\n   * // Should output:\n   * // id\n   * // slug\n   * ```\n   *\n   * @category Attribute Properties\n   */\n  getIdentifierAttributes(\n    options: {\n      filter?: PropertyFilterSync;\n      autoFork?: boolean;\n    } & CreatePropertyFilterOptionsForAttributes = {}\n  ) {\n    const {\n      filter: originalFilter,\n      attributeSelector = true,\n      setAttributesOnly = false,\n      autoFork = true\n    } = options;\n\n    const filter = function (this: Component, property: Property) {\n      if (!isIdentifierAttributeInstance(property)) {\n        return false;\n      }\n\n      if (originalFilter !== undefined) {\n        return originalFilter.call(this, property);\n      }\n\n      return true;\n    };\n\n    return this.getProperties<IdentifierAttribute>({\n      filter,\n      autoFork,\n      attributeSelector,\n      setAttributesOnly\n    });\n  }\n\n  /**\n   * Returns an iterator providing the secondary identifier attributes of the component.\n   *\n   * @param [options.filter] A function used to filter the secondary identifier attributes to be returned. The function is invoked for each identifier attribute with a [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute) instance as first argument.\n   * @param [options.setAttributesOnly] A boolean specifying whether only set secondary identifier attributes should be returned (default: `false`).\n   * @param [options.attributeSelector] An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the secondary identifier attributes to be returned (default: `true`, which means that all secondary identifier attributes should be returned).\n   *\n   * @returns A [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute) instance iterator.\n   *\n   * @example\n   * ```\n   * for (const attr of movie.getSecondaryIdentifierAttributes()) {\n   *   console.log(attr.getName());\n   * }\n   *\n   * // Should output:\n   * // slug\n   * ```\n   *\n   * @category Attribute Properties\n   */\n  getSecondaryIdentifierAttributes(\n    options: {\n      filter?: PropertyFilterSync;\n      autoFork?: boolean;\n    } & CreatePropertyFilterOptionsForAttributes = {}\n  ) {\n    const {\n      filter: originalFilter,\n      attributeSelector = true,\n      setAttributesOnly = false,\n      autoFork = true\n    } = options;\n\n    const filter = function (this: Component, property: Property) {\n      if (!isSecondaryIdentifierAttributeInstance(property)) {\n        return false;\n      }\n\n      if (originalFilter !== undefined) {\n        return originalFilter.call(this, property);\n      }\n\n      return true;\n    };\n\n    return this.getProperties<SecondaryIdentifierAttribute>({\n      filter,\n      autoFork,\n      attributeSelector,\n      setAttributesOnly\n    });\n  }\n\n  /**\n   * Returns an object composed of all the identifiers that are set in the component. The shape of the returned object is `{[identifierName]: identifierValue}`. Throws an error if the component doesn't have any set identifiers.\n   *\n   * @returns An object.\n   *\n   * @example\n   * ```\n   * movie.getIdentifiers(); // => {id: 'abc123', slug: 'inception'}\n   * ```\n   *\n   * @category Attribute Properties\n   */\n  getIdentifiers() {\n    const identifiers = this.__getIdentifiers();\n\n    if (identifiers === undefined) {\n      throw new Error(\n        `Cannot get the identifiers of a component that has no set identifier (${this.describeComponent()})`\n      );\n    }\n\n    return identifiers;\n  }\n\n  /**\n   * Returns whether the component has an identifier that is set or not.\n   *\n   * @returns A boolean.\n   *\n   * @example\n   * ```\n   * movie.hasIdentifiers(); // => true\n   * ```\n   *\n   * @category Attribute Properties\n   */\n  hasIdentifiers() {\n    return this.__getIdentifiers() !== undefined;\n  }\n\n  __getIdentifiers() {\n    let identifiers: NormalizedIdentifierDescriptor | undefined;\n\n    for (const identifierAttribute of this.getIdentifierAttributes({\n      setAttributesOnly: true,\n      autoFork: false\n    })) {\n      const name = identifierAttribute.getName();\n      const value = identifierAttribute.getValue() as IdentifierValue;\n\n      if (identifiers === undefined) {\n        identifiers = {};\n      }\n\n      identifiers[name] = value;\n    }\n\n    return identifiers;\n  }\n\n  /**\n   * Generates a unique identifier using the [cuid](https://github.com/ericelliott/cuid) library.\n   *\n   * @returns The generated identifier.\n   *\n   * @example\n   * ```\n   * Movie.generateId(); // => 'ck41vli1z00013h5xx1esffyn'\n   * ```\n   *\n   * @category Attribute Properties\n   */\n  static generateId() {\n    return cuid();\n  }\n\n  __getMinimumAttributeCount() {\n    return this.hasPrimaryIdentifierAttribute() ? 1 : 0;\n  }\n\n  // === Identifier Descriptor ===\n\n  /**\n   * Returns the `IdentifierDescriptor` of the component.\n   *\n   * An `IdentifierDescriptor` is a plain object composed of one pair of name/value corresponding to the name and value of the first identifier attribute encountered in a component. Usually it is the primary identifier, but if the primary identifier is not set, it can be a secondary identifier.\n   *\n   * If there is no set identifier in the component, an error is thrown.\n   *\n   * @returns An object.\n   *\n   * @example\n   * ```\n   * movie.getIdentifierDescriptor(); // => {id: 'abc123'}\n   * ```\n   *\n   * @category Identifier Descriptor\n   */\n  getIdentifierDescriptor() {\n    const identifierDescriptor = this.__getIdentifierDescriptor();\n\n    if (identifierDescriptor === undefined) {\n      throw new Error(\n        `Cannot get an identifier descriptor from a component that has no set identifier (${this.describeComponent()})`\n      );\n    }\n\n    return identifierDescriptor;\n  }\n\n  /**\n   * Returns whether the component can provide an `IdentifierDescriptor` (using the [`getIdentifierDescriptor()`](https://layrjs.com/docs/v2/reference/component#get-identifier-descriptor-instance-method) method) or not.\n   *\n   * @returns A boolean.\n   *\n   * @example\n   * ```\n   * movie.hasIdentifierDescriptor(); // => true\n   * ```\n   *\n   * @category Identifier Descriptor\n   */\n  hasIdentifierDescriptor() {\n    return this.__getIdentifierDescriptor() !== undefined;\n  }\n\n  __getIdentifierDescriptor(): NormalizedIdentifierDescriptor | undefined {\n    const primaryIdentifierAttribute = this.getPrimaryIdentifierAttribute();\n\n    if (primaryIdentifierAttribute.isSet()) {\n      const name = primaryIdentifierAttribute.getName();\n      const value = primaryIdentifierAttribute.getValue() as IdentifierValue;\n\n      return {[name]: value};\n    }\n\n    for (const secondaryIdentifierAttribute of this.getSecondaryIdentifierAttributes({\n      setAttributesOnly: true\n    })) {\n      const name = secondaryIdentifierAttribute.getName();\n      const value = secondaryIdentifierAttribute.getValue() as IdentifierValue;\n\n      return {[name]: value};\n    }\n\n    return undefined;\n  }\n\n  static normalizeIdentifierDescriptor(\n    identifierDescriptor: IdentifierDescriptor\n  ): NormalizedIdentifierDescriptor {\n    if (typeof identifierDescriptor === 'string' || typeof identifierDescriptor === 'number') {\n      const primaryIdentifierAttribute = this.prototype.getPrimaryIdentifierAttribute();\n      const name = primaryIdentifierAttribute.getName();\n      primaryIdentifierAttribute.checkValue(identifierDescriptor);\n\n      return {[name]: identifierDescriptor};\n    }\n\n    if (!isPlainObject(identifierDescriptor)) {\n      throw new Error(\n        `An identifier descriptor should be a string, a number, or an object, but received a value of type '${getTypeOf(\n          identifierDescriptor\n        )}' (${this.describeComponent()})`\n      );\n    }\n\n    const attributes = Object.entries(identifierDescriptor);\n\n    if (attributes.length !== 1) {\n      throw new Error(\n        `An identifier descriptor should be a string, a number, or an object composed of one attribute, but received an object composed of ${\n          attributes.length\n        } attributes (${this.describeComponent()}, received object: ${JSON.stringify(\n          identifierDescriptor\n        )})`\n      );\n    }\n\n    const [name, value] = attributes[0];\n    const identifierAttribute = this.prototype.getIdentifierAttribute(name);\n    identifierAttribute.checkValue(value);\n\n    return {[name]: value};\n  }\n\n  static describeIdentifierDescriptor(identifierDescriptor: IdentifierDescriptor) {\n    const normalizedIdentifierDescriptor = this.normalizeIdentifierDescriptor(identifierDescriptor);\n    const [[name, value]] = Object.entries(normalizedIdentifierDescriptor);\n    const valueString = typeof value === 'string' ? `'${value}'` : value.toString();\n\n    return `${name}: ${valueString}`;\n  }\n\n  // === Identifier Selector ====\n\n  static normalizeIdentifierSelector(\n    identifierSelector: IdentifierSelector\n  ): NormalizedIdentifierSelector {\n    if (typeof identifierSelector === 'string' || typeof identifierSelector === 'number') {\n      const primaryIdentifierAttribute = this.prototype.getPrimaryIdentifierAttribute();\n      const name = primaryIdentifierAttribute.getName();\n      primaryIdentifierAttribute.checkValue(identifierSelector);\n\n      return {[name]: identifierSelector};\n    }\n\n    if (!isPlainObject(identifierSelector)) {\n      throw new Error(\n        `An identifier selector should be a string, a number, or an object, but received a value of type '${getTypeOf(\n          identifierSelector\n        )}' (${this.describeComponent()})`\n      );\n    }\n\n    const attributes = Object.entries(identifierSelector);\n\n    if (attributes.length === 0) {\n      throw new Error(\n        `An identifier selector should be a string, a number, or a non-empty object, but received an empty object (${this.describeComponent()})`\n      );\n    }\n\n    const normalizedIdentifierSelector: NormalizedIdentifierSelector = {};\n\n    for (const [name, value] of attributes) {\n      const identifierAttribute = this.prototype.getIdentifierAttribute(name);\n      identifierAttribute.checkValue(value);\n      normalizedIdentifierSelector[name] = value;\n    }\n\n    return normalizedIdentifierSelector;\n  }\n\n  __createIdentifierSelectorFromObject(object: PlainObject) {\n    const identifierSelector: NormalizedIdentifierSelector = {};\n\n    for (const identifierAttribute of this.getIdentifierAttributes({autoFork: false})) {\n      const name = identifierAttribute.getName();\n      const value: IdentifierValue | undefined = object[name];\n\n      if (value !== undefined) {\n        identifierSelector[name] = value;\n      }\n    }\n\n    return identifierSelector;\n  }\n\n  // === Attribute Value Assignment ===\n\n  /**\n   * Assigns the specified attribute values to the current component class.\n   *\n   * @param object An object specifying the attribute values to assign.\n   * @param [options.source] A string specifying the [source](https://layrjs.com/docs/v2/reference/attribute#value-source-type) of the attribute values (default: `'local'`).\n   *\n   * @returns The current component class.\n   *\n   * @example\n   * ```\n   * import {Component, attribute} from '﹫layr/component';\n   *\n   * class Application extends Component {\n   *   ﹫attribute('string') static language = 'en';\n   * }\n   *\n   * Application.language // => 'en'\n   *\n   * Application.assign({language: 'fr'});\n   *\n   * Application.language // => 'fr'\n   * ```\n   *\n   * @category Attribute Value Assignment\n   */\n  static assign<T extends typeof Component>(\n    this: T,\n    object: PlainObject = {},\n    options: {\n      source?: ValueSource;\n    } = {}\n  ): T {\n    const {source} = options;\n\n    this.__assignAttributes(object, {source});\n\n    return this;\n  }\n\n  /**\n   * Assigns the specified attribute values to the current component instance.\n   *\n   * @param object An object specifying the attribute values to assign.\n   * @param [options.source] A string specifying the [source](https://layrjs.com/docs/v2/reference/attribute#value-source-type) of the attribute values (default: `'local'`).\n   *\n   * @returns The current component instance.\n   *\n   * @example\n   * ```\n   * // JS\n   *\n   * import {Component, attribute} from '﹫layr/component';\n   *\n   * class Movie extends Component {\n   *   ﹫attribute('string') title;\n   *   ﹫attribute('number') rating;\n   * }\n   *\n   * const movie = new Movie({title: 'Inception', rating: 8.7});\n   *\n   * movie.title // => 'Inception'\n   * movie.rating // => 8.7\n   *\n   * movie.assign({rating: 8.8});\n   *\n   * movie.title // => 'Inception'\n   * movie.rating // => 8.8\n   * ```\n   *\n   * @example\n   * ```\n   * // TS\n   *\n   * import {Component, attribute} from '﹫layr/component';\n   *\n   * class Movie extends Component {\n   *   ﹫attribute('string') title!: string;\n   *   ﹫attribute('number') rating!: number;\n   * }\n   *\n   * const movie = new Movie({title: 'Inception', rating: 8.7});\n   *\n   * movie.title // => 'Inception'\n   * movie.rating // => 8.7\n   *\n   * movie.assign({rating: 8.8});\n   *\n   * movie.rating // => 8.8\n   * ```\n   *\n   * @category Attribute Value Assignment\n   */\n  assign<T extends Component>(\n    this: T,\n    object: PlainObject = {},\n    options: {\n      source?: ValueSource;\n    } = {}\n  ): T {\n    const {source} = options;\n\n    this.__assignAttributes(object, {source});\n\n    return this;\n  }\n\n  static get __assignAttributes() {\n    return this.prototype.__assignAttributes;\n  }\n\n  __assignAttributes(object: PlainObject, {source}: {source: ValueSource | undefined}) {\n    for (const [name, value] of Object.entries(object)) {\n      this.getAttribute(name).setValue(value, {source});\n    }\n  }\n\n  // === Identity Mapping ===\n\n  static __identityMap: IdentityMap;\n\n  /**\n   * Gets the [`IdentityMap`](https://layrjs.com/docs/v2/reference/identity-map) of the component.\n   *\n   * @returns An [`IdentityMap`](https://layrjs.com/docs/v2/reference/identity-map) instance.\n   *\n   * @category Identity Mapping\n   */\n  static getIdentityMap() {\n    if (this.__identityMap === undefined) {\n      Object.defineProperty(this, '__identityMap', {value: new IdentityMap(this)});\n    } else if (!hasOwnProperty(this, '__identityMap')) {\n      Object.defineProperty(this, '__identityMap', {value: this.__identityMap.fork(this)});\n    }\n\n    return this.__identityMap;\n  }\n\n  static __isAttached: boolean;\n\n  /**\n   * Attaches the component class to its [`IdentityMap`](https://layrjs.com/docs/v2/reference/identity-map). By default, all component classes are attached, so unless you have detached a component class earlier, you should not have to use this method.\n   *\n   * @returns The component class.\n   *\n   * @category Identity Mapping\n   */\n  static attach<T extends typeof Component>(this: T) {\n    Object.defineProperty(this, '__isAttached', {value: true, configurable: true});\n\n    return this;\n  }\n\n  /**\n   * Detaches the component class from its [`IdentityMap`](https://layrjs.com/docs/v2/reference/identity-map).\n   *\n   * @returns The component class.\n   *\n   * @category Identity Mapping\n   */\n  static detach<T extends typeof Component>(this: T) {\n    Object.defineProperty(this, '__isAttached', {value: false, configurable: true});\n\n    return this;\n  }\n\n  /**\n   * Returns whether the component class is attached to its [`IdentityMap`](https://layrjs.com/docs/v2/reference/identity-map).\n   *\n   * @returns A boolean.\n   *\n   * @category Identity Mapping\n   */\n  static isAttached() {\n    let currentComponent = this;\n\n    while (true) {\n      const isAttached = currentComponent.__isAttached;\n\n      if (isAttached !== undefined) {\n        return isAttached;\n      }\n\n      const componentProvider = currentComponent.getComponentProvider();\n\n      if (componentProvider === currentComponent) {\n        return true;\n      }\n\n      currentComponent = componentProvider;\n    }\n  }\n\n  /**\n   * Returns whether the component class is detached from its [`IdentityMap`](https://layrjs.com/docs/v2/reference/identity-map).\n   *\n   * @returns A boolean.\n   *\n   * @category Identity Mapping\n   */\n  static isDetached() {\n    return !this.isAttached();\n  }\n\n  __isAttached?: boolean;\n\n  /**\n   * Attaches the component instance to its [`IdentityMap`](https://layrjs.com/docs/v2/reference/identity-map). By default, all component instances are attached, so unless you have detached a component instance earlier, you should not have to use this method.\n   *\n   * @returns The component instance.\n   *\n   * @category Identity Mapping\n   */\n  attach() {\n    Object.defineProperty(this, '__isAttached', {value: true, configurable: true});\n\n    if (this.hasPrimaryIdentifierAttribute()) {\n      const identityMap = this.constructor.getIdentityMap();\n      identityMap.addComponent(this);\n    }\n\n    return this;\n  }\n\n  /**\n   * Detaches the component instance from its [`IdentityMap`](https://layrjs.com/docs/v2/reference/identity-map).\n   *\n   * @returns The component instance.\n   *\n   * @category Identity Mapping\n   */\n  detach() {\n    if (this.hasPrimaryIdentifierAttribute()) {\n      const identityMap = this.constructor.getIdentityMap();\n      identityMap.removeComponent(this);\n    }\n\n    Object.defineProperty(this, '__isAttached', {value: false, configurable: true});\n\n    return this;\n  }\n\n  /**\n   * Returns whether the component instance is attached to its [`IdentityMap`](https://layrjs.com/docs/v2/reference/identity-map).\n   *\n   * @returns A boolean.\n   *\n   * @category Identity Mapping\n   */\n  isAttached() {\n    if (this.__isAttached !== undefined) {\n      return this.__isAttached;\n    }\n\n    return this.constructor.isAttached();\n  }\n\n  /**\n   * Returns whether the component instance is detached from its [`IdentityMap`](https://layrjs.com/docs/v2/reference/identity-map).\n   *\n   * @returns A boolean.\n   *\n   * @category Identity Mapping\n   */\n  isDetached() {\n    return !this.isAttached();\n  }\n\n  // === Attribute selectors ===\n\n  static get resolveAttributeSelector() {\n    return this.prototype.resolveAttributeSelector;\n  }\n\n  resolveAttributeSelector(\n    attributeSelector: AttributeSelector,\n    options: ResolveAttributeSelectorOptions = {}\n  ) {\n    attributeSelector = normalizeAttributeSelector(attributeSelector);\n\n    const {\n      filter,\n      setAttributesOnly = false,\n      target,\n      aggregationMode = 'union',\n      includeReferencedComponents = false,\n      alwaysIncludePrimaryIdentifierAttributes = true,\n      allowPartialArrayItems = true,\n      depth = Number.MAX_SAFE_INTEGER,\n      _isDeep = false,\n      _isArrayItem = false,\n      _attributeStack = new Set()\n    } = options;\n\n    const _skipUnchangedAttributes =\n      setAttributesOnly &&\n      target !== undefined &&\n      (target === 'client' || typeof (this as any).isStorable === 'function');\n\n    return this.__resolveAttributeSelector(attributeSelector, {\n      filter,\n      setAttributesOnly,\n      target,\n      aggregationMode,\n      includeReferencedComponents,\n      alwaysIncludePrimaryIdentifierAttributes,\n      allowPartialArrayItems,\n      depth,\n      _isDeep,\n      _skipUnchangedAttributes,\n      _isArrayItem,\n      _attributeStack\n    });\n  }\n\n  static get __resolveAttributeSelector() {\n    return this.prototype.__resolveAttributeSelector;\n  }\n\n  __resolveAttributeSelector(\n    attributeSelector: AttributeSelector,\n    options: ResolveAttributeSelectorOptions\n  ) {\n    const {\n      filter,\n      setAttributesOnly,\n      target,\n      includeReferencedComponents,\n      alwaysIncludePrimaryIdentifierAttributes,\n      allowPartialArrayItems,\n      depth,\n      _isDeep,\n      _skipUnchangedAttributes,\n      _isArrayItem,\n      _attributeStack\n    } = options;\n\n    if (depth! < 0) {\n      return attributeSelector;\n    }\n\n    const newDepth = depth! - 1;\n\n    let resolvedAttributeSelector: AttributeSelector = {};\n\n    if (attributeSelector === false) {\n      return resolvedAttributeSelector; // Optimization\n    }\n\n    const isEmbedded = isComponentInstance(this) && this.constructor.isEmbedded();\n\n    if (!setAttributesOnly && isEmbedded && _isArrayItem && !allowPartialArrayItems) {\n      attributeSelector = true;\n    }\n\n    // By default, referenced components are not resolved\n    if (!_isDeep || includeReferencedComponents || isEmbedded) {\n      for (const attribute of this.getAttributes({filter, setAttributesOnly})) {\n        const name = attribute.getName();\n\n        const subattributeSelector = getFromAttributeSelector(attributeSelector, name);\n\n        if (subattributeSelector === false) {\n          continue;\n        }\n\n        if (_skipUnchangedAttributes && attribute.getValueSource() === target) {\n          const valueType = attribute.getValueType();\n\n          const attributeIsReferencedComponent =\n            isComponentValueTypeInstance(valueType) &&\n            !ensureComponentClass(valueType.getComponent(attribute)).isEmbedded();\n\n          if (!attributeIsReferencedComponent) {\n            continue;\n          }\n        }\n\n        if (_attributeStack!.has(attribute)) {\n          continue; // Avoid looping indefinitely when a circular attribute is encountered\n        }\n\n        _attributeStack!.add(attribute);\n\n        const resolvedSubattributeSelector = attribute._resolveAttributeSelector(\n          subattributeSelector,\n          {...options, depth: newDepth, _isDeep: true}\n        );\n\n        _attributeStack!.delete(attribute);\n\n        if (resolvedSubattributeSelector !== false) {\n          resolvedAttributeSelector = setWithinAttributeSelector(\n            resolvedAttributeSelector,\n            name,\n            resolvedSubattributeSelector\n          );\n        }\n      }\n    }\n\n    if (\n      isComponentInstance(this) &&\n      alwaysIncludePrimaryIdentifierAttributes &&\n      this.hasPrimaryIdentifierAttribute()\n    ) {\n      const primaryIdentifierAttribute = this.getPrimaryIdentifierAttribute();\n\n      const isNotFilteredOut =\n        filter !== undefined ? filter.call(this, primaryIdentifierAttribute) : true;\n\n      if (isNotFilteredOut && (!setAttributesOnly || primaryIdentifierAttribute.isSet())) {\n        resolvedAttributeSelector = setWithinAttributeSelector(\n          resolvedAttributeSelector,\n          primaryIdentifierAttribute.getName(),\n          true\n        );\n      }\n    }\n\n    return resolvedAttributeSelector;\n  }\n\n  // === Validation ===\n\n  /**\n   * Validates the attributes of the component. If an attribute doesn't pass the validation, an error is thrown. The error is a JavaScript `Error` instance with a `failedValidators` custom attribute which contains the result of the [`runValidators()`](https://layrjs.com/docs/v2/reference/component#run-validators-dual-method) method.\n   *\n   * @param [attributeSelector] An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be validated (default: `true`, which means that all the attributes will be validated).\n   *\n   * @example\n   * ```\n   * // JS\n   *\n   * import {Component, attribute, validators} from '﹫layr/component';\n   *\n   * const {notEmpty} = validators;\n   *\n   * class Movie extends Component {\n   *   ﹫attribute('string', {validators: [notEmpty()]}) title;\n   * }\n   *\n   * const movie = new Movie({title: 'Inception'});\n   *\n   * movie.title; // => 'Inception'\n   * movie.validate(); // All good!\n   * movie.title = '';\n   * movie.validate(); // Error {failedValidators: [{validator: ..., path: 'title'}]}\n   * ```\n   *\n   * @example\n   * ```\n   * // TS\n   *\n   * import {Component, attribute, validators} from '﹫layr/component';\n   *\n   * const {notEmpty} = validators;\n   *\n   * class Movie extends Component {\n   *   ﹫attribute('string', {validators: [notEmpty()]}) title!: string;\n   * }\n   *\n   * const movie = new Movie({title: 'Inception'});\n   *\n   * movie.title; // => 'Inception'\n   * movie.validate(); // All good!\n   * movie.title = '';\n   * movie.validate(); // Error {failedValidators: [{validator: ..., path: 'title'}]}\n   * ```\n   *\n   * @category Validation\n   */\n  static get validate() {\n    return this.prototype.validate;\n  }\n\n  /**\n   * Validates the attributes of the component. If an attribute doesn't pass the validation, an error is thrown. The error is a JavaScript `Error` instance with a `failedValidators` custom attribute which contains the result of the [`runValidators()`](https://layrjs.com/docs/v2/reference/component#run-validators-dual-method) method.\n   *\n   * @param [attributeSelector] An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be validated (default: `true`, which means that all the attributes will be validated).\n   *\n   * @example\n   * ```\n   * // JS\n   *\n   * import {Component, attribute, validators} from '﹫layr/component';\n   *\n   * const {notEmpty} = validators;\n   *\n   * class Movie extends Component {\n   *   ﹫attribute('string', {validators: [notEmpty()]}) title;\n   * }\n   *\n   * const movie = new Movie({title: 'Inception'});\n   *\n   * movie.title; // => 'Inception'\n   * movie.validate(); // All good!\n   * movie.title = '';\n   * movie.validate(); // Error {failedValidators: [{validator: ..., path: 'title'}]}\n   * ```\n   *\n   * @example\n   * ```\n   * // TS\n   *\n   * import {Component, attribute, validators} from '﹫layr/component';\n   *\n   * const {notEmpty} = validators;\n   *\n   * class Movie extends Component {\n   *   ﹫attribute('string', {validators: [notEmpty()]}) title!: string;\n   * }\n   *\n   * const movie = new Movie({title: 'Inception'});\n   *\n   * movie.title; // => 'Inception'\n   * movie.validate(); // All good!\n   * movie.title = '';\n   * movie.validate(); // Error {failedValidators: [{validator: ..., path: 'title'}]}\n   * ```\n   *\n   * @category Validation\n   */\n  validate(attributeSelector: AttributeSelector = true) {\n    const failedValidators = this.runValidators(attributeSelector);\n\n    if (failedValidators.length === 0) {\n      return;\n    }\n\n    const details = failedValidators\n      .map(({validator, path}) => `${validator.getMessage()} (path: '${path}')`)\n      .join(', ');\n\n    let displayMessage: string | undefined;\n\n    for (const {validator} of failedValidators) {\n      const message = validator.getMessage({generateIfMissing: false});\n\n      if (message !== undefined) {\n        displayMessage = message;\n        break;\n      }\n    }\n\n    throwError(\n      `The following error(s) occurred while validating the component '${ensureComponentClass(\n        this\n      ).getComponentName()}': ${details}`,\n      {displayMessage, failedValidators}\n    );\n  }\n\n  /**\n   * Returns whether the attributes of the component are valid.\n   *\n   * @param [attributeSelector] An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be checked (default: `true`, which means that all the attributes will be checked).\n   *\n   * @returns A boolean.\n   *\n   * @example\n   * ```\n   * // See the `movie` definition in the `validate()` example\n   *\n   * movie.title; // => 'Inception'\n   * movie.isValid(); // => true\n   * movie.title = '';\n   * movie.isValid(); // => false\n   * ```\n   *\n   * @category Validation\n   */\n  static get isValid() {\n    return this.prototype.isValid;\n  }\n\n  /**\n   * Returns whether the attributes of the component are valid.\n   *\n   * @param [attributeSelector] An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be checked (default: `true`, which means that all the attributes will be checked).\n   *\n   * @returns A boolean.\n   *\n   * @example\n   * ```\n   * // See the `movie` definition in the `validate()` example\n   *\n   * movie.title; // => 'Inception'\n   * movie.isValid(); // => true\n   * movie.title = '';\n   * movie.isValid(); // => false\n   * ```\n   *\n   * @category Validation\n   */\n  isValid(attributeSelector: AttributeSelector = true) {\n    const failedValidators = this.runValidators(attributeSelector);\n\n    return failedValidators.length === 0;\n  }\n\n  /**\n   * Runs the validators for all the set attributes of the component.\n   *\n   * @param [attributeSelector] An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be validated (default: `true`, which means that all the attributes will be validated).\n   *\n   * @returns An array containing the validators that have failed. Each item is a plain object composed of a `validator` (a [`Validator`](https://layrjs.com/docs/v2/reference/validator) instance) and a `path` (a string representing the path of the attribute containing the validator that has failed).\n   *\n   * @example\n   * ```\n   * // See the `movie` definition in the `validate()` example\n   *\n   * movie.title; // => 'Inception'\n   * movie.runValidators(); // => []\n   * movie.title = '';\n   * movie.runValidators(); // => [{validator: ..., path: 'title'}]\n   * ```\n   *\n   * @category Validation\n   */\n  static get runValidators() {\n    return this.prototype.runValidators;\n  }\n\n  /**\n   * Runs the validators for all the set attributes of the component.\n   *\n   * @param [attributeSelector] An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be validated (default: `true`, which means that all the attributes will be validated).\n   *\n   * @returns An array containing the validators that have failed. Each item is a plain object composed of a `validator` (a [`Validator`](https://layrjs.com/docs/v2/reference/validator) instance) and a `path` (a string representing the path of the attribute containing the validator that has failed).\n   *\n   * @example\n   * ```\n   * // See the `movie` definition in the `validate()` example\n   *\n   * movie.title; // => 'Inception'\n   * movie.runValidators(); // => []\n   * movie.title = '';\n   * movie.runValidators(); // => [{validator: ..., path: 'title'}]\n   * ```\n   *\n   * @category Validation\n   */\n  runValidators(attributeSelector: AttributeSelector = true) {\n    attributeSelector = this.resolveAttributeSelector(attributeSelector);\n\n    const failedValidators = [];\n\n    for (const attribute of this.getAttributes({setAttributesOnly: true})) {\n      const name = attribute.getName();\n\n      const subattributeSelector = getFromAttributeSelector(attributeSelector, name);\n\n      if (subattributeSelector === false) {\n        continue;\n      }\n\n      const attributeFailedValidators = attribute.runValidators(subattributeSelector);\n\n      for (const {validator, path} of attributeFailedValidators) {\n        failedValidators.push({validator, path: joinAttributePath([name, path])});\n      }\n    }\n\n    return failedValidators;\n  }\n\n  // === Method Properties ===\n\n  /**\n   * Gets a method of the component.\n   *\n   * @param name The name of the method to get.\n   *\n   * @returns A [`Method`](https://layrjs.com/docs/v2/reference/method) instance.\n   *\n   * @example\n   * ```\n   * movie.getMethod('play'); // => 'play' method property\n   * movie.getMethod('title'); // => Error ('title' is an attribute property)\n   * ```\n   *\n   * @category Method Properties\n   */\n  static get getMethod() {\n    return this.prototype.getMethod;\n  }\n\n  /**\n   * Gets a method of the component.\n   *\n   * @param name The name of the method to get.\n   *\n   * @returns A [`Method`](https://layrjs.com/docs/v2/reference/method) instance.\n   *\n   * @example\n   * ```\n   * movie.getMethod('play'); // => 'play' method property\n   * movie.getMethod('title'); // => Error ('title' is an attribute property)\n   * ```\n   *\n   * @category Method Properties\n   */\n  getMethod(name: string, options: {autoFork?: boolean} = {}) {\n    const {autoFork = true} = options;\n\n    const method = this.__getMethod(name, {autoFork});\n\n    if (method === undefined) {\n      throw new Error(`The method '${name}' is missing (${this.describeComponent()})`);\n    }\n\n    return method;\n  }\n\n  /**\n   * Returns whether the component has the specified method.\n   *\n   * @param name The name of the method to check.\n   *\n   * @returns A boolean.\n   *\n   * @example\n   * ```\n   * movie.hasMethod('play'); // => true\n   * movie.hasMethod('destroy'); // => false\n   * movie.hasMethod('title'); // => Error ('title' is an attribute property)\n   * ```\n   *\n   * @category Method Properties\n   */\n  static get hasMethod() {\n    return this.prototype.hasMethod;\n  }\n\n  /**\n   * Returns whether the component has the specified method.\n   *\n   * @param name The name of the method to check.\n   *\n   * @returns A boolean.\n   *\n   * @example\n   * ```\n   * movie.hasMethod('play'); // => true\n   * movie.hasMethod('destroy'); // => false\n   * movie.hasMethod('title'); // => Error ('title' is an attribute property)\n   * ```\n   *\n   * @category Method Properties\n   */\n  hasMethod(name: string) {\n    return this.__getMethod(name, {autoFork: false}) !== undefined;\n  }\n\n  static get __getMethod() {\n    return this.prototype.__getMethod;\n  }\n\n  __getMethod(name: string, options: {autoFork: boolean}) {\n    const {autoFork} = options;\n\n    const property = this.__getProperty(name, {autoFork});\n\n    if (property === undefined) {\n      return undefined;\n    }\n\n    if (!isMethodInstance(property)) {\n      throw new Error(\n        `A property with the specified name was found, but it is not a method (${property.describe()})`\n      );\n    }\n\n    return property;\n  }\n\n  /**\n   * Defines a method in the component. Typically, instead of using this method, you would rather use the [`@method()`](https://layrjs.com/docs/v2/reference/component#method-decorator) decorator.\n   *\n   * @param name The name of the method to define.\n   * @param [methodOptions] The options to create the [`Method`](https://layrjs.com/docs/v2/reference/method#constructor).\n   *\n   * @returns The [`Method`](https://layrjs.com/docs/v2/reference/method) that was created.\n   *\n   * @example\n   * ```\n   * Movie.prototype.setMethod('play');\n   * ```\n   *\n   * @category Method Properties\n   */\n  static get setMethod() {\n    return this.prototype.setMethod;\n  }\n\n  /**\n   * Defines a method in the component. Typically, instead of using this method, you would rather use the [`@method()`](https://layrjs.com/docs/v2/reference/component#method-decorator) decorator.\n   *\n   * @param name The name of the method to define.\n   * @param [methodOptions] The options to create the [`Method`](https://layrjs.com/docs/v2/reference/method#constructor).\n   *\n   * @returns The [`Method`](https://layrjs.com/docs/v2/reference/method) that was created.\n   *\n   * @example\n   * ```\n   * Movie.prototype.setMethod('play');\n   * ```\n   *\n   * @category Method Properties\n   */\n  setMethod(name: string, methodOptions: MethodOptions = {}) {\n    return this.setProperty(name, Method, methodOptions);\n  }\n\n  /**\n   * Returns an iterator providing the methods of the component.\n   *\n   * @param [options.filter] A function used to filter the methods to be returned. The function is invoked for each method with a [`Method`](https://layrjs.com/docs/v2/reference/method) instance as first argument.\n   *\n   * @returns A [`Method`](https://layrjs.com/docs/v2/reference/method) instance iterator.\n   *\n   * @example\n   * ```\n   * for (const meth of movie.getMethods()) {\n   *   console.log(meth.getName());\n   * }\n   *\n   * // Should output:\n   * // play\n   * ```\n   *\n   * @category Method Properties\n   */\n  static get getMethods() {\n    return this.prototype.getMethods;\n  }\n\n  /**\n   * Returns an iterator providing the methods of the component.\n   *\n   * @param [options.filter] A function used to filter the methods to be returned. The function is invoked for each method with a [`Method`](https://layrjs.com/docs/v2/reference/method) instance as first argument.\n   *\n   * @returns A [`Method`](https://layrjs.com/docs/v2/reference/method) instance iterator.\n   *\n   * @example\n   * ```\n   * for (const meth of movie.getMethods()) {\n   *   console.log(meth.getName());\n   * }\n   *\n   * // Should output:\n   * // play\n   * ```\n   *\n   * @category Method Properties\n   */\n  getMethods(options: {filter?: PropertyFilterSync; autoFork?: boolean} = {}) {\n    const {filter, autoFork = true} = options;\n\n    return this.getProperties<Method>({filter, autoFork, methodsOnly: true});\n  }\n\n  // === Dependency Management ===\n\n  // --- Component getters ---\n\n  /**\n   * Gets a component class that is provided or consumed by the current component. An error is thrown if there is no component matching the specified name. If the specified name is the name of the current component, the latter is returned.\n   *\n   * @param name The name of the component class to get.\n   *\n   * @returns A component class.\n   *\n   * @example\n   * ```\n   * class Application extends Component {\n   *   ﹫provide() static Movie = Movie;\n   * }\n   *\n   * Application.getComponent('Movie'); // => Movie\n   * Application.getComponent('Application'); // => Application\n   * ```\n   *\n   * @category Dependency Management\n   */\n  static getComponent(name: string) {\n    const component = this.__getComponent(name);\n\n    if (component === undefined) {\n      throw new Error(\n        `Cannot get the component '${name}' from the component '${this.getComponentPath()}'`\n      );\n    }\n\n    return component;\n  }\n\n  /**\n   * Returns whether the current component provides or consumes another component.\n   *\n   * @param name The name of the component class to check.\n   *\n   * @returns A boolean.\n   *\n   * @example\n   * ```\n   * class Application extends Component {\n   *   ﹫provide() static Movie = Movie;\n   * }\n   *\n   * Application.hasComponent('Movie'); // => true\n   * Application.hasComponent('Application'); // => true\n   * Application.hasComponent('Film'); // => false\n   * ```\n   *\n   * @category Dependency Management\n   */\n  static hasComponent(name: string) {\n    return this.__getComponent(name) !== undefined;\n  }\n\n  static __getComponent(name: string): typeof Component | undefined {\n    assertIsComponentName(name);\n\n    if (this.getComponentName() === name) {\n      return this;\n    }\n\n    let providedComponent = this.getProvidedComponent(name);\n\n    if (providedComponent !== undefined) {\n      return providedComponent;\n    }\n\n    const componentProvider = this.__getComponentProvider();\n\n    if (componentProvider !== undefined) {\n      return componentProvider.__getComponent(name);\n    }\n\n    return undefined;\n  }\n\n  /**\n   * Gets a component class or prototype of the specified type that is provided or consumed by the current component. An error is thrown if there is no component matching the specified type. If the specified type is the type of the current component, the latter is returned.\n   *\n   * @param type The type of the component class or prototype to get.\n   *\n   * @returns A component class or prototype.\n   *\n   * @example\n   * ```\n   * class Application extends Component {\n   *   ﹫provide() static Movie = Movie;\n   * }\n   *\n   * Application.getComponentOfType('typeof Movie'); // => Movie\n   * Application.getComponentOfType('Movie'); // => Movie.prototype\n   * Application.getComponentOfType('typeof Application'); // => Application\n   * Application.getComponentOfType('Application'); // => Application.prototype\n   * ```\n   *\n   * @category Dependency Management\n   */\n  static getComponentOfType(type: string) {\n    const component = this.__getComponentOfType(type);\n\n    if (component === undefined) {\n      throw new Error(\n        `Cannot get the component of type '${type}' from the component '${this.getComponentPath()}'`\n      );\n    }\n\n    return component;\n  }\n\n  /**\n   * Returns whether the current component provides or consumes a component class or prototype matching the specified type.\n   *\n   * @param type The type of the component class or prototype to check.\n   *\n   * @returns A boolean.\n   *\n   * @example\n   * ```\n   * class Application extends Component {\n   *   ﹫provide() static Movie = Movie;\n   * }\n   *\n   * Application.hasComponentOfType('typeof Movie'); // => true\n   * Application.hasComponentOfType('Movie'); // => true\n   * Application.hasComponentOfType('typeof Application'); // => true\n   * Application.hasComponentOfType('Application'); // => true\n   * Application.hasComponentOfType('typeof Film'); // => false\n   * Application.hasComponentOfType('Film'); // => false\n   * ```\n   *\n   * @category Dependency Management\n   */\n  static hasComponentOfType(type: string) {\n    return this.__getComponentOfType(type) !== undefined;\n  }\n\n  static __getComponentOfType(type: string) {\n    const isComponentClassType = assertIsComponentType(type) === 'componentClassType';\n\n    const componentName = isComponentClassType\n      ? getComponentNameFromComponentClassType(type)\n      : getComponentNameFromComponentInstanceType(type);\n\n    const component = this.__getComponent(componentName);\n\n    if (component === undefined) {\n      return undefined;\n    }\n\n    return isComponentClassType ? component : component.prototype;\n  }\n\n  static traverseComponents(options: {filter?: (component: typeof Component) => boolean} = {}) {\n    const {filter} = options;\n\n    const component = this;\n\n    return {\n      *[Symbol.iterator](): Generator<typeof Component> {\n        if (filter === undefined || filter(component)) {\n          yield component;\n        }\n\n        for (const providedComponent of component.getProvidedComponents({deep: true, filter})) {\n          yield providedComponent;\n        }\n      }\n    };\n  }\n\n  // --- Component provision ---\n\n  /**\n   * Gets a component that is provided by the current component. An error is thrown if there is no provided component with the specified name.\n   *\n   * @param name The name of the provided component to get.\n   *\n   * @returns A component class.\n   *\n   * @example\n   * ```\n   * class Application extends Component {\n   *   ﹫provide() static Movie = Movie;\n   * }\n   *\n   * Application.getProvidedComponent('Movie'); // => Movie\n   * ```\n   *\n   * @category Dependency Management\n   */\n  static getProvidedComponent(name: string) {\n    assertIsComponentName(name);\n\n    const providedComponents = this.__getProvidedComponents();\n\n    let providedComponent = providedComponents[name];\n\n    if (providedComponent === undefined) {\n      return undefined;\n    }\n\n    if (!hasOwnProperty(providedComponents, name)) {\n      // Since the host component has been forked, the provided component must be forked as well\n      providedComponent = providedComponent.fork({componentProvider: this});\n      providedComponents[name] = providedComponent;\n    }\n\n    return providedComponent;\n  }\n\n  /**\n   * Specifies that the current component is providing another component so it can be easily accessed from the current component or from any component that is \"consuming\" it using the [`consumeComponent()`](https://layrjs.com/docs/v2/reference/component#consume-component-class-method) method or the [`@consume()`](https://layrjs.com/docs/v2/reference/component#consume-decorator) decorator.\n   *\n   * The provided component can later be accessed using a component accessor that was automatically set on the component provider.\n   *\n   * Typically, instead of using this method, you would rather use the [`@provide()`]((https://layrjs.com/docs/v2/reference/component#provide-decorator)) decorator.\n   *\n   * @param component The component class to provide.\n   *\n   * @example\n   * ```\n   * class Application extends Component {}\n   * class Movie extends Component {}\n   * Application.provideComponent(Movie);\n   *\n   * Application.Movie; // => `Movie` class\n   * ```\n   *\n   * @category Dependency Management\n   */\n  static provideComponent(component: typeof Component) {\n    assertIsComponentClass(component);\n\n    const providedComponents = this.__getProvidedComponents();\n\n    const existingProvider = component.__getComponentProvider();\n\n    if (existingProvider !== undefined) {\n      if (existingProvider === this) {\n        return;\n      }\n\n      throw new Error(\n        `Cannot provide the component '${component.getComponentName()}' from '${this.getComponentName()}' because '${component.getComponentName()}' is already provided by '${existingProvider.getComponentName()}'`\n      );\n    }\n\n    const componentName = component.getComponentName();\n\n    const existingComponent = providedComponents[componentName];\n\n    if (existingComponent !== undefined && !component.isForkOf(existingComponent)) {\n      throw new Error(\n        `Cannot provide the component '${component.getComponentName()}' from '${this.getComponentName()}' because a component with the same name is already provided`\n      );\n    }\n\n    if (componentName in this) {\n      const descriptor = Object.getOwnPropertyDescriptor(this, componentName);\n      const value = descriptor?.value;\n\n      if (!(isComponentClass(value) && (value === component || value.isForkOf(component)))) {\n        throw new Error(\n          `Cannot provide the component '${component.getComponentName()}' from '${this.getComponentName()}' because there is an existing property with the same name`\n        );\n      }\n    }\n\n    component.__setComponentProvider(this);\n    providedComponents[componentName] = component;\n\n    Object.defineProperty(this, componentName, {\n      get<T extends typeof Component>(this: T) {\n        return this.getProvidedComponent(componentName);\n      },\n      set<T extends typeof Component>(this: T, component: typeof Component) {\n        // Set the value temporarily so @provide() can get it\n        Object.defineProperty(this, componentName, {\n          value: component,\n          configurable: true,\n          enumerable: true,\n          writable: true\n        });\n      }\n    });\n  }\n\n  /**\n   * Returns an iterator allowing to iterate over the components provided by the current component.\n   *\n   * @param [options.filter] A function used to filter the provided components to be returned. The function is invoked for each provided component with the provided component as first argument.\n   * @param [options.deep] A boolean specifying whether the method should get the provided components recursively (i.e., get the provided components of the provided components). Default: `false`.\n   *\n   * @returns A component iterator.\n   *\n   * @category Dependency Management\n   */\n  static getProvidedComponents(\n    options: {deep?: boolean; filter?: (providedComponent: typeof Component) => boolean} = {}\n  ) {\n    const {deep = false, filter} = options;\n\n    const component = this;\n\n    return {\n      *[Symbol.iterator](): Generator<typeof Component> {\n        for (const name in component.__getProvidedComponents()) {\n          const providedComponent = component.getProvidedComponent(name)!;\n\n          if (filter !== undefined && !filter(providedComponent)) {\n            continue;\n          }\n\n          yield providedComponent;\n\n          if (deep) {\n            for (const nestedProvidedComponent of providedComponent.getProvidedComponents({\n              deep,\n              filter\n            })) {\n              yield nestedProvidedComponent;\n            }\n          }\n        }\n      }\n    };\n  }\n\n  /**\n   * Returns the provider of the component. If there is no component provider, returns the current component.\n   *\n   * @returns A component provider.\n   *\n   * @example\n   * ```\n   * class Application extends Component {}\n   * class Movie extends Component {}\n   * Application.provideComponent(Movie);\n   *\n   * Movie.getComponentProvider(); // => `Application` class\n   * Application.getComponentProvider(); // => `Application` class\n   * ```\n   *\n   * @category Dependency Management\n   */\n  static getComponentProvider() {\n    const componentName = this.getComponentName();\n\n    let currentComponent = this;\n\n    while (true) {\n      const componentProvider = currentComponent.__getComponentProvider();\n\n      if (componentProvider === undefined) {\n        return currentComponent;\n      }\n\n      const providedComponent = componentProvider.getProvidedComponent(componentName);\n\n      if (providedComponent !== undefined) {\n        return componentProvider;\n      }\n\n      currentComponent = componentProvider;\n    }\n  }\n\n  static __componentProvider?: typeof Component;\n\n  static __getComponentProvider() {\n    return hasOwnProperty(this, '__componentProvider') ? this.__componentProvider : undefined;\n  }\n\n  static __setComponentProvider(componentProvider: typeof Component) {\n    Object.defineProperty(this, '__componentProvider', {value: componentProvider});\n  }\n\n  static __providedComponents: {[name: string]: typeof Component};\n\n  static __getProvidedComponents() {\n    if (this.__providedComponents === undefined) {\n      Object.defineProperty(this, '__providedComponents', {\n        value: Object.create(null)\n      });\n    } else if (!hasOwnProperty(this, '__providedComponents')) {\n      Object.defineProperty(this, '__providedComponents', {\n        value: Object.create(this.__providedComponents)\n      });\n    }\n\n    return this.__providedComponents;\n  }\n\n  // --- Component consumption ---\n\n  /**\n   * Gets a component that is consumed by the current component. An error is thrown if there is no consumed component with the specified name. Typically, instead of using this method, you would rather use the component accessor that has been automatically set for you.\n   *\n   * @param name The name of the consumed component to get.\n   *\n   * @returns A component class.\n   *\n   * @example\n   * ```\n   * // JS\n   *\n   * class Movie extends Component {\n   *   ﹫consume() static Actor;\n   * }\n   *\n   * class Actor extends Component {}\n   *\n   * class Application extends Component {\n   *   ﹫provide() static Movie = Movie;\n   *   ﹫provide() static Actor = Actor;\n   * }\n   *\n   * Movie.getConsumedComponent('Actor'); // => Actor\n   *\n   * // Typically, you would rather use the component accessor:\n   * Movie.Actor; // => Actor\n   * ```\n   *\n   * @example\n   * ```\n   * // TS\n   *\n   * class Movie extends Component {\n   *   ﹫consume() static Actor: typeof Actor;\n   * }\n   *\n   * class Actor extends Component {}\n   *\n   * class Application extends Component {\n   *   ﹫provide() static Movie = Movie;\n   *   ﹫provide() static Actor = Actor;\n   * }\n   *\n   * Movie.getConsumedComponent('Actor'); // => Actor\n   *\n   * // Typically, you would rather use the component accessor:\n   * Movie.Actor; // => Actor\n   * ```\n   *\n   * @category Dependency Management\n   */\n  static getConsumedComponent(name: string) {\n    assertIsComponentName(name);\n\n    const consumedComponents = this.__getConsumedComponents();\n\n    if (!consumedComponents.has(name)) {\n      return undefined;\n    }\n\n    const componentProvider = this.__getComponentProvider();\n\n    if (componentProvider === undefined) {\n      return undefined;\n    }\n\n    return componentProvider.__getComponent(name);\n  }\n\n  /**\n   * Specifies that the current component is consuming another component so it can be easily accessed using a component accessor.\n   *\n   * Typically, instead of using this method, you would rather use the [`@consume()`]((https://layrjs.com/docs/v2/reference/component#consume-decorator)) decorator.\n   *\n   * @param name The name of the component to consume.\n   *\n   * @example\n   * ```\n   * class Application extends Component {}\n   * class Movie extends Component {}\n   * Application.provideComponent(Movie);\n   * Movie.consumeComponent('Application');\n   *\n   * Movie.Application; // => `Application` class\n   * ```\n   *\n   * @category Dependency Management\n   */\n  static consumeComponent(name: string) {\n    assertIsComponentName(name);\n\n    const consumedComponents = this.__getConsumedComponents(true);\n\n    if (consumedComponents.has(name)) {\n      return;\n    }\n\n    if (name in this) {\n      throw new Error(\n        `Cannot consume the component '${name}' from '${this.getComponentName()}' because there is an existing property with the same name`\n      );\n    }\n\n    consumedComponents.add(name);\n\n    Object.defineProperty(this, name, {\n      get<T extends typeof Component>(this: T) {\n        return this.getConsumedComponent(name);\n      },\n      set<T extends typeof Component>(this: T, _value: never) {\n        // A component consumer should not be set directly\n      }\n    });\n  }\n\n  /**\n   * Returns an iterator allowing to iterate over the components consumed by the current component.\n   *\n   * @param [options.filter] A function used to filter the consumed components to be returned. The function is invoked for each consumed component with the consumed component as first argument.\n   *\n   * @returns A component iterator.\n   *\n   * @category Dependency Management\n   */\n  static getConsumedComponents(\n    options: {filter?: (consumedComponent: typeof Component) => boolean} = {}\n  ) {\n    const {filter} = options;\n\n    const component = this;\n\n    return {\n      *[Symbol.iterator]() {\n        for (const name of component.__getConsumedComponents()) {\n          const consumedComponent = component.getConsumedComponent(name)!;\n\n          if (filter !== undefined && !filter(consumedComponent)) {\n            continue;\n          }\n\n          yield consumedComponent;\n        }\n      }\n    };\n  }\n\n  static __consumedComponents: Set<string>;\n\n  static __getConsumedComponents(autoFork = false) {\n    if (this.__consumedComponents === undefined) {\n      Object.defineProperty(this, '__consumedComponents', {\n        value: new Set()\n      });\n    } else if (autoFork && !hasOwnProperty(this, '__consumedComponents')) {\n      Object.defineProperty(this, '__consumedComponents', {\n        value: new Set(this.__consumedComponents)\n      });\n    }\n\n    return this.__consumedComponents;\n  }\n\n  // === Cloning ===\n\n  static clone() {\n    return this;\n  }\n\n  /**\n   * Clones the component instance. All primitive attributes are copied, and embedded components are cloned recursively. Currently, identifiable components (i.e., components having an identifier attribute) cannot be cloned, but this might change in the future.\n   *\n   * @returns A clone of the component.\n   *\n   * @example\n   * ```\n   * movie.title = 'Inception';\n   *\n   * const movieClone = movie.clone();\n   * movieClone.title = 'Inception 2';\n   *\n   * movieClone.title; // => 'Inception 2'\n   * movie.title; // => 'Inception'\n   * ```\n   *\n   * @category Cloning\n   * @possiblyasync\n   */\n  clone<T extends Component>(this: T, options: CloneOptions = {}): T {\n    if (this.hasPrimaryIdentifierAttribute()) {\n      return this;\n    }\n\n    const clonedComponent = this.constructor.instantiate() as T;\n\n    clonedComponent.setIsNewMark(this.getIsNewMark());\n\n    for (const attribute of this.getAttributes({setAttributesOnly: true})) {\n      const name = attribute.getName();\n      const value = attribute.getValue();\n      const source = attribute.getValueSource();\n      const clonedValue = clone(value, options);\n      clonedComponent.getAttribute(name).setValue(clonedValue, {source});\n    }\n\n    return clonedComponent;\n  }\n\n  // === Forking ===\n\n  /**\n   * Creates a fork of the component class.\n   *\n   * @returns The component class fork.\n   *\n   * @example\n   * ```\n   * class Movie extends Component {}\n   *\n   * Movie.fork(); // => A fork of the `Movie` class\n   * ```\n   *\n   * @category Forking\n   */\n  static fork<T extends typeof Component>(this: T, options: ForkOptions = {}): T {\n    const {componentProvider = this.__getComponentProvider()} = options;\n\n    const name = this.getComponentName();\n\n    // Use a little trick to make sure the generated subclass\n    // has the 'name' attribute set properly\n    // @ts-ignore\n    const {[name]: componentFork} = {[name]: class extends this {}};\n\n    if (componentFork.name !== name) {\n      // In case the code has been transpiled by Babel with @babel/plugin-transform-classes,\n      // the above trick doesn't work, so let's set the class name manually\n      Object.defineProperty(componentFork, 'name', {value: name});\n    }\n\n    if (componentProvider !== undefined) {\n      componentFork.__setComponentProvider(componentProvider);\n    }\n\n    return componentFork;\n  }\n\n  /**\n   * Creates a fork of the component instance. Note that the constructor of the resulting component will be a fork of the component class.\n   *\n   * @returns The component instance fork.\n   *\n   * @example\n   * ```\n   * class Movie extends Component {}\n   * const movie = new Movie();\n   *\n   * movie.fork(); // => A fork of `movie`\n   * movie.fork().constructor.isForkOf(Movie); // => true\n   * ```\n   *\n   * @category Forking\n   */\n  fork<T extends Component>(this: T, options: ForkOptions = {}) {\n    let {componentClass} = options;\n\n    if (componentClass === undefined) {\n      componentClass = this.constructor.fork();\n    } else {\n      assertIsComponentClass(componentClass);\n    }\n\n    const componentFork = Object.create(this) as T;\n\n    if (this.constructor !== componentClass) {\n      // Make 'componentFork' believe that it is an instance of 'Component'\n      // It can happen when a referenced component is forked\n      Object.defineProperty(componentFork, 'constructor', {\n        value: componentClass,\n        writable: true,\n        enumerable: false,\n        configurable: true\n      });\n\n      if (componentFork.hasPrimaryIdentifierAttribute() && componentFork.isAttached()) {\n        componentClass.getIdentityMap().addComponent(componentFork);\n      }\n    }\n\n    return componentFork;\n  }\n\n  /**\n   * Returns whether the component class is a fork of another component class.\n   *\n   * @returns A boolean.\n   *\n   * @example\n   * ```\n   * class Movie extends Component {}\n   * const MovieFork = Movie.fork();\n   *\n   * MovieFork.isForkOf(Movie); // => true\n   * Movie.isForkOf(MovieFork); // => false\n   * ```\n   *\n   * @category Forking\n   */\n  static isForkOf(component: typeof Component) {\n    assertIsComponentClass(component);\n\n    return isPrototypeOf(component, this);\n  }\n\n  /**\n   * Returns whether the component instance is a fork of another component instance.\n   *\n   * @returns A boolean.\n   *\n   * @example\n   * ```\n   * class Movie extends Component {}\n   * const movie = new Movie();\n   * const movieFork = movie.fork();\n   *\n   * movieFork.isForkOf(movie); // => true\n   * movie.isForkOf(movieFork); // => false\n   * ```\n   *\n   * @category Forking\n   */\n  isForkOf(component: Component) {\n    assertIsComponentInstance(component);\n\n    return isPrototypeOf(component, this);\n  }\n\n  static __ghost: typeof Component;\n\n  /**\n   * Gets the ghost of the component class. A ghost is like a fork, but it is unique. The first time you call this method, a fork is created, and then, all the successive calls return the same fork.\n   *\n   * @returns The ghost of the component class.\n   *\n   * @example\n   * ```\n   * class Movie extends Component {}\n   *\n   * Movie.getGhost() // => A fork of the `Movie` class\n   * Movie.getGhost() // => The same fork of the `Movie` class\n   * ```\n   *\n   * @category Forking\n   */\n  static getGhost<T extends typeof Component>(this: T) {\n    let ghost = this.__ghost;\n\n    if (ghost === undefined) {\n      const componentProvider = this.getComponentProvider();\n\n      if (componentProvider === this) {\n        ghost = this.fork();\n      } else {\n        ghost = componentProvider.getGhost().getComponent(this.getComponentName());\n      }\n\n      Object.defineProperty(this, '__ghost', {value: ghost});\n    }\n\n    return ghost as T;\n  }\n\n  /**\n   * Gets the ghost of the component instance. A ghost is like a fork, but it is unique. The first time you call this method, a fork is created, and then, all the successive calls return the same fork. Only identifiable components (i.e., components having an identifier attribute) can be \"ghosted\".\n   *\n   * @returns The ghost of the component instance.\n   *\n   * @example\n   * ```\n   * // JS\n   *\n   * class Movie extends Component {\n   *   ﹫primaryIdentifier() id;\n   * }\n   *\n   * const movie = new Movie();\n   *\n   * movie.getGhost() // => A fork of `movie`\n   * movie.getGhost() // => The same fork of `movie`\n   * ```\n   *\n   * @example\n   * ```\n   * // TS\n   *\n   * class Movie extends Component {\n   *   ﹫primaryIdentifier() id!: string;\n   * }\n   *\n   * const movie = new Movie();\n   *\n   * movie.getGhost() // => A fork of `movie`\n   * movie.getGhost() // => The same fork of `movie`\n   * ```\n   *\n   * @category Forking\n   */\n  getGhost<T extends Component>(this: T): T {\n    const identifiers = this.getIdentifiers();\n    const ghostClass = this.constructor.getGhost();\n    const ghostIdentityMap = ghostClass.getIdentityMap();\n    let ghostComponent = ghostIdentityMap.getComponent(identifiers);\n\n    if (ghostComponent === undefined) {\n      ghostComponent = this.fork({componentClass: ghostClass});\n      ghostIdentityMap.addComponent(ghostComponent);\n    }\n\n    return ghostComponent as T;\n  }\n\n  // === Merging ===\n\n  /**\n   * Merges the attributes of a component class fork into the current component class.\n   *\n   * @param componentFork The component class fork to merge.\n   *\n   * @returns The current component class.\n   *\n   * @example\n   * ```\n   * class Movie extends Component {\n   *   ﹫attribute('string') static customName = 'Movie';\n   * }\n   *\n   * const MovieFork = Movie.fork();\n   * MovieFork.customName = 'Film';\n   *\n   * Movie.customName; // => 'Movie'\n   * Movie.merge(MovieFork);\n   * Movie.customName; // => 'Film'\n   * ```\n   *\n   * @category Merging\n   */\n  static merge<T extends typeof Component>(\n    this: T,\n    componentFork: typeof Component,\n    options: MergeOptions & {attributeSelector?: AttributeSelector} = {}\n  ) {\n    assertIsComponentClass(componentFork);\n\n    if (!isPrototypeOf(this, componentFork)) {\n      throw new Error('Cannot merge a component that is not a fork of the target component');\n    }\n\n    this.__mergeAttributes(componentFork, options);\n\n    return this;\n  }\n\n  /**\n   * Merges the attributes of a component instance fork into the current component instance.\n   *\n   * @param componentFork The component instance fork to merge.\n   *\n   * @returns The current component instance.\n   *\n   * @example\n   * ```\n   * const movie = new Movie({title: 'Inception'});\n   * const movieFork = movie.fork();\n   * movieFork.title = 'Inception 2';\n   *\n   * movie.title; // => 'Inception'\n   * movie.merge(movieFork);\n   * movie.title; // => 'Inception 2'\n   * ```\n   *\n   * @category Merging\n   */\n  merge(\n    componentFork: Component,\n    options: MergeOptions & {attributeSelector?: AttributeSelector} = {}\n  ) {\n    assertIsComponentInstance(componentFork);\n\n    if (!isPrototypeOf(this, componentFork)) {\n      throw new Error('Cannot merge a component that is not a fork of the target component');\n    }\n\n    this.__mergeAttributes(componentFork, options);\n\n    return this;\n  }\n\n  static get __mergeAttributes() {\n    return this.prototype.__mergeAttributes;\n  }\n\n  __mergeAttributes(\n    componentFork: typeof Component | Component,\n    {attributeSelector, ...otherOptions}: MergeOptions & {attributeSelector?: AttributeSelector}\n  ) {\n    for (const attributeFork of componentFork.getAttributes({attributeSelector})) {\n      const name = attributeFork.getName();\n\n      const attribute = this.getAttribute(name);\n\n      if (!attributeFork.isSet()) {\n        if (attribute.isSet()) {\n          attribute.unsetValue();\n        }\n\n        continue;\n      }\n\n      const valueFork = attributeFork.getValue();\n      const value = attribute.getValue({throwIfUnset: false});\n\n      const mergedValue = merge(value, valueFork, otherOptions);\n\n      attribute.setValue(mergedValue, {source: attributeFork.getValueSource()});\n    }\n  }\n\n  // === Serialization ===\n\n  /**\n   * Serializes the component class to a plain object.\n   *\n   * @param [options.attributeSelector] An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be serialized (default: `true`, which means that all the attributes will be serialized).\n   * @param [options.attributeFilter] A (possibly async) function used to filter the attributes to be serialized. The function is invoked for each attribute with an [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) instance as first argument.\n   * @param [options.target] A string specifying the [target](https://layrjs.com/docs/v2/reference/attribute#value-source-type) of the serialization (default: `undefined`).\n   *\n   * @returns A plain object representing the serialized component class.\n   *\n   * @example\n   * ```\n   * class Movie extends Component {\n   *   ﹫attribute('string') static customName = 'Film';\n   * }\n   *\n   * Movie.serialize(); // => {__component: 'typeof Movie', customName: 'Film'}\n   * ```\n   *\n   * @category Serialization\n   * @possiblyasync\n   */\n  static serialize(options: SerializeOptions = {}) {\n    const {\n      attributeSelector = true,\n      serializedComponents = new Set(),\n      returnComponentReferences = false,\n      ignoreEmptyComponents = false,\n      includeComponentTypes = true,\n      includeReferencedComponents = false,\n      target,\n      ...otherOptions\n    } = options;\n\n    const resolvedAttributeSelector = this.resolveAttributeSelector(attributeSelector, {\n      setAttributesOnly: true,\n      target,\n      aggregationMode: 'intersection',\n      includeReferencedComponents\n    });\n\n    return this.__serialize({\n      ...otherOptions,\n      attributeSelector: resolvedAttributeSelector,\n      serializedComponents,\n      returnComponentReferences,\n      ignoreEmptyComponents,\n      includeComponentTypes,\n      includeReferencedComponents,\n      target\n    });\n  }\n\n  static __serialize(options: SerializeOptions) {\n    const {\n      serializedComponents,\n      componentDependencies,\n      returnComponentReferences,\n      ignoreEmptyComponents,\n      includeComponentTypes,\n      includeReferencedComponents\n    } = options;\n\n    const serializedComponent: PlainObject = {};\n\n    if (includeComponentTypes) {\n      serializedComponent.__component = this.getComponentType();\n    }\n\n    const hasAlreadyBeenSerialized = serializedComponents!.has(this);\n\n    if (!hasAlreadyBeenSerialized) {\n      serializedComponents!.add(this);\n\n      if (componentDependencies !== undefined) {\n        for (const providedComponent of this.getProvidedComponents()) {\n          componentDependencies.add(providedComponent);\n        }\n\n        for (const consumedComponent of this.getConsumedComponents()) {\n          componentDependencies.add(consumedComponent);\n        }\n      }\n    }\n\n    if (hasAlreadyBeenSerialized || (returnComponentReferences && !includeReferencedComponents)) {\n      return serializedComponent;\n    }\n\n    return possiblyAsync(\n      this.__serializeAttributes(serializedComponent, options),\n      (attributeCount) =>\n        ignoreEmptyComponents && attributeCount === 0 ? undefined : serializedComponent\n    );\n  }\n\n  /**\n   * Serializes the component instance to a plain object.\n   *\n   * @param [options.attributeSelector] An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be serialized (default: `true`, which means that all the attributes will be serialized).\n   * @param [options.attributeFilter] A (possibly async) function used to filter the attributes to be serialized. The function is invoked for each attribute with an [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) instance as first argument.\n   * @param [options.target] A string specifying the [target](https://layrjs.com/docs/v2/reference/attribute#value-source-type) of the serialization (default: `undefined`).\n   *\n   * @returns A plain object representing the serialized component instance.\n   *\n   * @example\n   * ```\n   * const movie = new Movie({title: 'Inception'});\n   *\n   * movie.serialize(); // => {__component: 'Movie', title: 'Inception'}\n   * ```\n   *\n   * @category Serialization\n   * @possiblyasync\n   */\n  serialize(options: SerializeOptions = {}) {\n    const {\n      attributeSelector = true,\n      serializedComponents = new Set(),\n      returnComponentReferences = false,\n      ignoreEmptyComponents = false,\n      includeComponentTypes = true,\n      includeIsNewMarks = true,\n      includeReferencedComponents = false,\n      target,\n      ...otherOptions\n    } = options;\n\n    const resolvedAttributeSelector = this.resolveAttributeSelector(attributeSelector, {\n      setAttributesOnly: true,\n      target,\n      aggregationMode: 'intersection',\n      includeReferencedComponents\n    });\n\n    return this.__serialize({\n      ...otherOptions,\n      attributeSelector: resolvedAttributeSelector,\n      serializedComponents,\n      returnComponentReferences,\n      ignoreEmptyComponents,\n      includeComponentTypes,\n      includeIsNewMarks,\n      includeReferencedComponents,\n      target\n    });\n  }\n\n  __serialize(options: SerializeOptions) {\n    const {\n      serializedComponents,\n      componentDependencies,\n      returnComponentReferences,\n      ignoreEmptyComponents,\n      includeComponentTypes,\n      includeIsNewMarks,\n      includeReferencedComponents\n    } = options;\n\n    const serializedComponent: PlainObject = {};\n\n    if (includeComponentTypes) {\n      serializedComponent.__component = this.getComponentType();\n    }\n\n    const isEmbedded = this.constructor.isEmbedded();\n\n    if (!isEmbedded) {\n      const hasAlreadyBeenSerialized = serializedComponents!.has(this);\n\n      if (!hasAlreadyBeenSerialized) {\n        if (componentDependencies !== undefined) {\n          componentDependencies.add(this.constructor);\n\n          for (const providedComponent of this.constructor.getProvidedComponents()) {\n            componentDependencies.add(providedComponent);\n          }\n\n          for (const consumedComponent of this.constructor.getConsumedComponents()) {\n            componentDependencies.add(consumedComponent);\n          }\n        }\n      }\n\n      if (hasAlreadyBeenSerialized || (returnComponentReferences && !includeReferencedComponents)) {\n        Object.assign(serializedComponent, this.getIdentifierDescriptor());\n\n        return serializedComponent;\n      }\n\n      serializedComponents!.add(this);\n    }\n\n    const isNew = this.getIsNewMark();\n\n    if (isNew && includeIsNewMarks) {\n      serializedComponent.__new = true;\n    }\n\n    return possiblyAsync(\n      this.__serializeAttributes(serializedComponent, options),\n      (attributeCount) =>\n        ignoreEmptyComponents && attributeCount <= this.__getMinimumAttributeCount()\n          ? undefined\n          : serializedComponent\n    );\n  }\n\n  static get __serializeAttributes() {\n    return this.prototype.__serializeAttributes;\n  }\n\n  __serializeAttributes(\n    serializedComponent: PlainObject,\n    options: SerializeOptions\n  ): PromiseLikeable<number> {\n    let {attributeSelector, attributeFilter} = options;\n\n    let attributeCount = 0;\n\n    return possiblyAsync(\n      possiblyAsync.forEach(this.getAttributes({attributeSelector}), (attribute) => {\n        const attributeName = attribute.getName();\n        const subattributeSelector = getFromAttributeSelector(attributeSelector!, attributeName);\n\n        return possiblyAsync(\n          attributeFilter !== undefined ? attributeFilter.call(this, attribute) : true,\n          (isNotFilteredOut) => {\n            if (isNotFilteredOut) {\n              return possiblyAsync(\n                attribute.serialize({\n                  ...options,\n                  attributeSelector: subattributeSelector,\n                  returnComponentReferences: true\n                }),\n                (serializedAttributeValue) => {\n                  serializedComponent[attributeName] = serializedAttributeValue;\n                  attributeCount++;\n                }\n              );\n            }\n          }\n        );\n      }),\n      () => attributeCount\n    );\n  }\n\n  // === Deserialization ===\n\n  /**\n   * Deserializes the component class from the specified plain object. The deserialization operates \"in place\", which means that the current component class attributes are mutated.\n   *\n   * @param [object] The plain object to deserialize from.\n   * @param [options.attributeFilter] A (possibly async) function used to filter the attributes to be deserialized. The function is invoked for each attribute with an [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) instance as first argument.\n   * @param [options.source] A string specifying the [source](https://layrjs.com/docs/v2/reference/attribute#value-source-type) of the serialization (default: `'local'`).\n   *\n   * @returns The component class.\n   *\n   * @example\n   * ```\n   * class Movie extends Component {\n   *   ﹫attribute('string') static customName = 'Movie';\n   * }\n   *\n   * Movie.customName; // => 'Movie'\n   * Movie.deserialize({customName: 'Film'});\n   * Movie.customName; // => 'Film'\n   * ```\n   *\n   * @category Deserialization\n   * @possiblyasync\n   */\n  static deserialize<T extends typeof Component>(\n    this: T,\n    object: PlainObject = {},\n    options: DeserializeOptions = {}\n  ): T | PromiseLike<T> {\n    const {__component: componentType, ...attributes} = object;\n    const {deserializedComponents} = options;\n\n    if (componentType !== undefined) {\n      const expectedComponentType = this.getComponentType();\n\n      if (componentType !== expectedComponentType) {\n        throw new Error(\n          `An unexpected component type was encountered while deserializing an object (encountered type: '${componentType}', expected type: '${expectedComponentType}')`\n        );\n      }\n    }\n\n    if (deserializedComponents !== undefined) {\n      deserializedComponents.add(this);\n    }\n\n    return possiblyAsync(this.__deserializeAttributes(attributes, options), () => this);\n  }\n\n  /**\n   * Deserializes the component instance from the specified plain object. The deserialization operates \"in place\", which means that the current component instance attributes are mutated.\n   *\n   * @param [object] The plain object to deserialize from.\n   * @param [options.attributeFilter] A (possibly async) function used to filter the attributes to be deserialized. The function is invoked for each attribute with an [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) instance as first argument.\n   * @param [options.source] A string specifying the [source](https://layrjs.com/docs/v2/reference/attribute#value-source-type) of the serialization (default: `'local'`).\n   *\n   * @returns The current component instance.\n   *\n   * @example\n   * ```\n   * class Movie extends Component {\n   *   ﹫attribute('string') title = '';\n   * }\n   *\n   * const movie = new Movie();\n   *\n   * movie.title; // => ''\n   * movie.deserialize({title: 'Inception'});\n   * movie.title; // => 'Inception'\n   * ```\n   *\n   * @category Deserialization\n   * @possiblyasync\n   */\n  deserialize<T extends Component>(\n    this: T,\n    object: PlainObject = {},\n    options: DeserializeOptions = {}\n  ): T | PromiseLike<T> {\n    const {deserializedComponents} = options;\n\n    const {__component: componentType, __new: isNew = false, ...attributes} = object;\n\n    if (componentType !== undefined) {\n      const expectedComponentType = this.getComponentType();\n\n      if (componentType !== expectedComponentType) {\n        throw new Error(\n          `An unexpected component type was encountered while deserializing an object (encountered type: '${componentType}', expected type: '${expectedComponentType}')`\n        );\n      }\n    }\n\n    if (isNew && !this.getIsNewMark()) {\n      throw new Error(\n        `Cannot mark as new an existing non-new component (${this.describeComponent()})`\n      );\n    }\n\n    this.setIsNewMark(isNew);\n\n    if (deserializedComponents !== undefined && !this.constructor.isEmbedded()) {\n      deserializedComponents.add(this);\n    }\n\n    return possiblyAsync(this.__deserializeAttributes(attributes, options), () => this);\n  }\n\n  static get __deserializeAttributes() {\n    return this.prototype.__deserializeAttributes;\n  }\n\n  __deserializeAttributes(\n    serializedAttributes: PlainObject,\n    options: DeserializeOptions\n  ): void | PromiseLike<void> {\n    const {attributeFilter} = options;\n\n    return possiblyAsync.forEach(\n      Object.entries(serializedAttributes),\n      ([attributeName, serializedAttributeValue]: [string, unknown]) => {\n        const attribute = this.getAttribute(attributeName);\n\n        return possiblyAsync(\n          attributeFilter !== undefined ? attributeFilter.call(this, attribute) : true,\n          (isNotFilteredOut) => {\n            if (isNotFilteredOut) {\n              return attribute.deserialize(serializedAttributeValue, options);\n            }\n          }\n        );\n      }\n    );\n  }\n\n  // === Execution mode ===\n\n  static __executionMode: ExecutionMode;\n\n  static getExecutionMode() {\n    let currentComponent = this;\n\n    while (true) {\n      const executionMode = currentComponent.__executionMode;\n\n      if (executionMode !== undefined) {\n        return executionMode;\n      }\n\n      const componentProvider = currentComponent.getComponentProvider();\n\n      if (componentProvider === currentComponent) {\n        return 'foreground';\n      }\n\n      currentComponent = componentProvider;\n    }\n  }\n\n  static setExecutionMode(executionMode: ExecutionMode) {\n    Object.defineProperty(this, '__executionMode', {value: executionMode});\n  }\n\n  // === Introspection ===\n\n  static introspect({\n    _introspectedComponents = new Map()\n  }: {_introspectedComponents?: IntrospectedComponentMap} = {}) {\n    if (_introspectedComponents.has(this)) {\n      return _introspectedComponents.get(this);\n    }\n\n    let introspectedComponent: IntrospectedComponent | undefined;\n\n    const introspectedProperties = this.__introspectProperties();\n    const introspectedPrototypeProperties = this.prototype.__introspectProperties();\n    const introspectedProvidedComponents = this.__introspectProvidedComponents({\n      _introspectedComponents\n    });\n\n    if (\n      introspectedProperties.length > 0 ||\n      introspectedPrototypeProperties.length > 0 ||\n      introspectedProvidedComponents.length > 0\n    ) {\n      introspectedComponent = {\n        name: this.getComponentName()\n      };\n    }\n\n    _introspectedComponents.set(this, introspectedComponent);\n\n    if (introspectedComponent === undefined) {\n      return undefined;\n    }\n\n    if (this.isEmbedded()) {\n      introspectedComponent.isEmbedded = true;\n    }\n\n    const introspectedMixins = this.__introspectMixins();\n\n    if (introspectedMixins.length > 0) {\n      introspectedComponent.mixins = introspectedMixins;\n    }\n\n    if (introspectedProperties.length > 0) {\n      introspectedComponent.properties = introspectedProperties;\n    }\n\n    if (introspectedPrototypeProperties.length > 0) {\n      introspectedComponent.prototype = {properties: introspectedPrototypeProperties};\n    }\n\n    if (introspectedProvidedComponents.length > 0) {\n      introspectedComponent.providedComponents = introspectedProvidedComponents;\n    }\n\n    const introspectedConsumedComponents = this.__introspectConsumedComponents({\n      _introspectedComponents\n    });\n\n    if (introspectedConsumedComponents.length > 0) {\n      introspectedComponent.consumedComponents = introspectedConsumedComponents;\n    }\n\n    return introspectedComponent;\n  }\n\n  static __introspectMixins() {\n    const introspectedMixins = new Array<string>();\n\n    let currentClass = this;\n\n    while (isComponentClass(currentClass)) {\n      if (hasOwnProperty(currentClass, '__mixin')) {\n        const mixinName = (currentClass as any).__mixin;\n\n        if (!introspectedMixins.includes(mixinName)) {\n          introspectedMixins.unshift(mixinName);\n        }\n      }\n\n      currentClass = Object.getPrototypeOf(currentClass);\n    }\n\n    return introspectedMixins;\n  }\n\n  static get __introspectProperties() {\n    return this.prototype.__introspectProperties;\n  }\n\n  __introspectProperties() {\n    const introspectedProperties = [];\n\n    for (const property of this.getProperties({autoFork: false})) {\n      const introspectedProperty = property.introspect();\n\n      if (introspectedProperty !== undefined) {\n        introspectedProperties.push(introspectedProperty);\n      }\n    }\n\n    return introspectedProperties;\n  }\n\n  static __introspectProvidedComponents({\n    _introspectedComponents\n  }: {\n    _introspectedComponents: IntrospectedComponentMap;\n  }) {\n    const introspectedProvidedComponents = [];\n\n    for (const providedComponent of this.getProvidedComponents()) {\n      const introspectedProvidedComponent = providedComponent.introspect({_introspectedComponents});\n\n      if (introspectedProvidedComponent !== undefined) {\n        introspectedProvidedComponents.push(introspectedProvidedComponent);\n      }\n    }\n\n    return introspectedProvidedComponents;\n  }\n\n  static __introspectConsumedComponents({\n    _introspectedComponents\n  }: {\n    _introspectedComponents: IntrospectedComponentMap;\n  }) {\n    const introspectedConsumedComponents = [];\n\n    for (const consumedComponent of this.getConsumedComponents()) {\n      const introspectedConsumedComponent = consumedComponent.introspect({_introspectedComponents});\n\n      if (introspectedConsumedComponent !== undefined) {\n        introspectedConsumedComponents.push(consumedComponent.getComponentName());\n      }\n    }\n\n    return introspectedConsumedComponents;\n  }\n\n  static unintrospect(\n    introspectedComponent: IntrospectedComponent,\n    options: {mixins?: ComponentMixin[]; methodBuilder?: MethodBuilder} = {}\n  ): typeof Component {\n    const {\n      name,\n      isEmbedded,\n      mixins: introspectedMixins,\n      properties: introspectedProperties,\n      prototype: {properties: introspectedPrototypeProperties} = {},\n      providedComponents: introspectedProvidedComponents,\n      consumedComponents: introspectedConsumedComponents\n    } = introspectedComponent;\n\n    const {mixins = [], methodBuilder} = options;\n\n    let UnintrospectedComponent = class extends Component {};\n\n    if (isEmbedded) {\n      UnintrospectedComponent.isEmbedded = function () {\n        return true;\n      };\n    }\n\n    if (introspectedMixins !== undefined) {\n      UnintrospectedComponent = UnintrospectedComponent.__unintrospectMixins(introspectedMixins, {\n        mixins\n      });\n    }\n\n    const propertyClassGetter = UnintrospectedComponent.getPropertyClass;\n\n    if (introspectedProperties !== undefined) {\n      UnintrospectedComponent.__unintrospectProperties(\n        introspectedProperties,\n        propertyClassGetter,\n        {methodBuilder}\n      );\n    }\n\n    if (introspectedPrototypeProperties !== undefined) {\n      UnintrospectedComponent.prototype.__unintrospectProperties(\n        introspectedPrototypeProperties,\n        propertyClassGetter,\n        {methodBuilder}\n      );\n    }\n\n    UnintrospectedComponent.setComponentName(name);\n\n    if (introspectedProvidedComponents !== undefined) {\n      UnintrospectedComponent.__unintrospectProvidedComponents(introspectedProvidedComponents, {\n        mixins,\n        methodBuilder\n      });\n    }\n\n    if (introspectedConsumedComponents !== undefined) {\n      UnintrospectedComponent.__unintrospectConsumedComponents(introspectedConsumedComponents);\n    }\n\n    UnintrospectedComponent.__setRemoteComponent(UnintrospectedComponent);\n\n    if (methodBuilder !== undefined) {\n      UnintrospectedComponent.__setRemoteMethodBuilder(methodBuilder);\n    }\n\n    return UnintrospectedComponent;\n  }\n\n  static __unintrospectMixins(introspectedMixins: string[], {mixins}: {mixins: ComponentMixin[]}) {\n    let UnintrospectedComponentWithMixins = this;\n\n    for (const mixinName of introspectedMixins) {\n      const Mixin = mixins.find((Mixin) => getFunctionName(Mixin) === mixinName);\n\n      if (Mixin === undefined) {\n        throw new Error(\n          `Couldn't find a component mixin named '${mixinName}'. Please make sure you specified it when creating your 'ComponentClient'.`\n        );\n      }\n\n      UnintrospectedComponentWithMixins = Mixin(UnintrospectedComponentWithMixins);\n    }\n\n    return UnintrospectedComponentWithMixins;\n  }\n\n  static get __unintrospectProperties() {\n    return this.prototype.__unintrospectProperties;\n  }\n\n  __unintrospectProperties(\n    introspectedProperties: IntrospectedProperty[],\n    propertyClassGetter: typeof Component['getPropertyClass'],\n    {methodBuilder}: {methodBuilder: MethodBuilder | undefined}\n  ) {\n    for (const introspectedProperty of introspectedProperties) {\n      const {type} = introspectedProperty;\n      const PropertyClass = propertyClassGetter.call(ensureComponentClass(this), type);\n      const {name, options} = PropertyClass.unintrospect(introspectedProperty);\n      const property = this.setProperty(name, PropertyClass, options);\n\n      if (isAttributeInstance(property)) {\n        if (property.getExposure()?.set !== true) {\n          property.markAsControlled();\n        }\n      } else if (isMethodInstance(property)) {\n        if (\n          property.getExposure()?.call === true &&\n          methodBuilder !== undefined &&\n          !(name in this)\n        ) {\n          Object.defineProperty(this, name, {\n            value: methodBuilder(name),\n            writable: true,\n            enumerable: false,\n            configurable: true\n          });\n        }\n      }\n    }\n  }\n\n  static __unintrospectProvidedComponents(\n    introspectedProvidedComponents: IntrospectedComponent[],\n    {\n      mixins,\n      methodBuilder\n    }: {mixins: ComponentMixin[] | undefined; methodBuilder: MethodBuilder | undefined}\n  ) {\n    for (const introspectedProvidedComponent of introspectedProvidedComponents) {\n      this.provideComponent(\n        Component.unintrospect(introspectedProvidedComponent, {mixins, methodBuilder})\n      );\n    }\n  }\n\n  static __unintrospectConsumedComponents(introspectedConsumedComponents: string[]) {\n    for (const introspectedConsumedComponent of introspectedConsumedComponents) {\n      this.consumeComponent(introspectedConsumedComponent);\n    }\n  }\n\n  // === Remote component ===\n\n  static __remoteComponent: typeof Component | undefined;\n\n  static getRemoteComponent() {\n    return this.__remoteComponent;\n  }\n\n  getRemoteComponent() {\n    return this.constructor.getRemoteComponent()?.prototype;\n  }\n\n  static __setRemoteComponent(remoteComponent: typeof Component) {\n    Object.defineProperty(this, '__remoteComponent', {value: remoteComponent});\n  }\n\n  // === Remote methods ===\n\n  static get hasRemoteMethod() {\n    return this.prototype.hasRemoteMethod;\n  }\n\n  hasRemoteMethod(name: string) {\n    const remoteComponent = this.getRemoteComponent();\n\n    if (!remoteComponent?.hasMethod(name)) {\n      return false;\n    }\n\n    const remoteMethod = remoteComponent.getMethod(name, {autoFork: false});\n\n    return remoteMethod.getExposure()?.call === true;\n  }\n\n  static get callRemoteMethod() {\n    return this.prototype.callRemoteMethod;\n  }\n\n  callRemoteMethod(name: string, ...args: any[]): any {\n    const remoteMethodBuilder = ensureComponentClass(this).__remoteMethodBuilder;\n\n    if (remoteMethodBuilder === undefined) {\n      throw new Error(\n        `Cannot call the remote method '${name}' for a component that does not come from a component client (${this.describeComponent()})`\n      );\n    }\n\n    return remoteMethodBuilder(name).apply(this, args);\n  }\n\n  static __remoteMethodBuilder: MethodBuilder | undefined;\n\n  static __setRemoteMethodBuilder(methodBuilder: MethodBuilder) {\n    Object.defineProperty(this, '__remoteMethodBuilder', {value: methodBuilder});\n  }\n\n  // === Utilities ===\n\n  static isComponent(value: any): value is Component {\n    return isComponentInstance(value);\n  }\n\n  static get toObject() {\n    return this.prototype.toObject;\n  }\n\n  toObject(options: {minimize?: boolean} = {}) {\n    const {minimize = false} = options;\n\n    if (minimize) {\n      if (isComponentClass(this)) {\n        return {};\n      }\n\n      if (this.hasIdentifiers()) {\n        return this.getIdentifierDescriptor();\n      }\n    }\n\n    const object: PlainObject = {};\n\n    const handleValue = (value: unknown): unknown => {\n      if (isComponentClassOrInstance(value)) {\n        const component = value;\n\n        if (!ensureComponentClass(component).isEmbedded()) {\n          return component.toObject({minimize: true});\n        } else {\n          return component.toObject({minimize});\n        }\n      }\n\n      if (Array.isArray(value)) {\n        return value.map(handleValue);\n      }\n\n      return value;\n    };\n\n    for (const attribute of this.getAttributes({setAttributesOnly: true})) {\n      object[attribute.getName()] = handleValue(attribute.getValue());\n    }\n\n    return object;\n  }\n\n  static get describeComponent() {\n    return this.prototype.describeComponent;\n  }\n\n  describeComponent(options: {componentPrefix?: string} = {}) {\n    let {componentPrefix = ''} = options;\n\n    if (componentPrefix !== '') {\n      componentPrefix = `${componentPrefix} `;\n    }\n\n    return `${componentPrefix}component: '${ensureComponentClass(this).getComponentPath()}'`;\n  }\n\n  static describeComponentProperty(name: string) {\n    return `${this.getComponentPath()}.${name}`;\n  }\n\n  describeComponentProperty(name: string) {\n    return `${this.constructor.getComponentPath()}.prototype.${name}`;\n  }\n}\n\n// The following would be better defined inside the Component class\n// but it leads to a TypeScript (4.3) compilation error in transient dependencies\nObject.defineProperty(Component, Symbol.hasInstance, {\n  value: function (instance: any) {\n    // Since fork() can change the constructor of the instance forks,\n    // we must change the behavior of 'instanceof' so it can work as expected\n    return instance.constructor === this || isPrototypeOf(this, instance.constructor);\n  }\n});\n\ntype CreatePropertyFilterOptions = {\n  attributesOnly?: boolean;\n  methodsOnly?: boolean;\n} & CreatePropertyFilterOptionsForAttributes;\n\ntype CreatePropertyFilterOptionsForAttributes = {\n  attributeSelector?: AttributeSelector;\n  setAttributesOnly?: boolean;\n};\n\nfunction createPropertyFilter(\n  originalFilter?: PropertyFilterSync,\n  options: CreatePropertyFilterOptions = {}\n) {\n  const {\n    attributesOnly = false,\n    attributeSelector = true,\n    setAttributesOnly = false,\n    methodsOnly = false\n  } = options;\n\n  const normalizedAttributeSelector = normalizeAttributeSelector(attributeSelector);\n\n  const filter = function (this: typeof Component | Component, property: Property) {\n    if (isAttributeInstance(property)) {\n      const attribute = property;\n\n      if (setAttributesOnly && !attribute.isSet()) {\n        return false;\n      }\n\n      const name = attribute.getName();\n\n      if (getFromAttributeSelector(normalizedAttributeSelector, name) === false) {\n        return false;\n      }\n    } else if (attributesOnly) {\n      return false;\n    }\n\n    if (isMethodInstance(property)) {\n      // NOOP\n    } else if (methodsOnly) {\n      return false;\n    }\n\n    if (originalFilter !== undefined) {\n      return originalFilter.call(this, property);\n    }\n\n    return true;\n  };\n\n  return filter;\n}\n"
  },
  {
    "path": "packages/component/src/decorators.test.ts",
    "content": "import {Component} from './component';\nimport {\n  isAttributeInstance,\n  isPrimaryIdentifierAttributeInstance,\n  isSecondaryIdentifierAttributeInstance,\n  isStringValueTypeInstance,\n  isNumberValueTypeInstance,\n  isMethodInstance\n} from './properties';\nimport {\n  attribute,\n  primaryIdentifier,\n  secondaryIdentifier,\n  method,\n  expose,\n  provide,\n  consume\n} from './decorators';\n\ndescribe('Decorators', () => {\n  test('@attribute()', async () => {\n    class Movie extends Component {\n      @attribute() static limit = 100;\n      @attribute() static token?: string;\n\n      @attribute() title? = '';\n      @attribute() country?: string;\n    }\n\n    let attr = Movie.getAttribute('limit');\n\n    expect(isAttributeInstance(attr)).toBe(true);\n    expect(attr.getName()).toBe('limit');\n    expect(attr.getParent()).toBe(Movie);\n    expect(attr.getValue()).toBe(100);\n    expect(Movie.limit).toBe(100);\n\n    Movie.limit = 500;\n\n    expect(attr.getValue()).toBe(500);\n    expect(Movie.limit).toBe(500);\n\n    let descriptor = Object.getOwnPropertyDescriptor(Movie, 'limit')!;\n\n    expect(typeof descriptor.get).toBe('function');\n    expect(typeof descriptor.set).toBe('function');\n\n    attr = Movie.getAttribute('token');\n\n    expect(isAttributeInstance(attr)).toBe(true);\n    expect(attr.getName()).toBe('token');\n    expect(attr.getParent()).toBe(Movie);\n    expect(attr.getValue()).toBeUndefined();\n    expect(Movie.token).toBeUndefined();\n\n    let movie = new Movie();\n\n    attr = movie.getAttribute('title');\n\n    expect(isAttributeInstance(attr)).toBe(true);\n    expect(attr.getName()).toBe('title');\n    expect(attr.getParent()).toBe(movie);\n    expect(typeof attr.getDefault()).toBe('function');\n    expect(attr.evaluateDefault()).toBe('');\n    expect(attr.getValue()).toBe('');\n    expect(movie.title).toBe('');\n\n    movie.title = 'The Matrix';\n\n    expect(attr.getValue()).toBe('The Matrix');\n    expect(movie.title).toBe('The Matrix');\n\n    descriptor = Object.getOwnPropertyDescriptor(Movie.prototype, 'title')!;\n\n    expect(typeof descriptor.get).toBe('function');\n    expect(typeof descriptor.set).toBe('function');\n\n    expect(Object.getOwnPropertyDescriptor(movie, 'title')).toBe(undefined);\n\n    attr = movie.getAttribute('country');\n\n    expect(isAttributeInstance(attr)).toBe(true);\n    expect(attr.getName()).toBe('country');\n    expect(attr.getParent()).toBe(movie);\n    expect(attr.getDefault()).toBeUndefined();\n    expect(attr.evaluateDefault()).toBeUndefined();\n    expect(attr.getValue()).toBeUndefined();\n    expect(movie.country).toBeUndefined();\n\n    movie.country = 'USA';\n\n    expect(attr.getValue()).toBe('USA');\n    expect(movie.country).toBe('USA');\n\n    expect(Movie.hasAttribute('offset')).toBe(false);\n    expect(() => Movie.getAttribute('offset')).toThrow(\n      \"The attribute 'offset' is missing (component: 'Movie')\"\n    );\n\n    movie = new Movie({title: 'Inception', country: 'USA'});\n\n    expect(movie.title).toBe('Inception');\n    expect(movie.country).toBe('USA');\n\n    class Film extends Movie {\n      @attribute() static limit: number;\n      @attribute() static token = '';\n\n      @attribute() title!: string;\n      @attribute() country = '';\n    }\n\n    attr = Film.getAttribute('limit');\n\n    expect(isAttributeInstance(attr)).toBe(true);\n    expect(attr.getName()).toBe('limit');\n    expect(attr.getParent()).toBe(Film);\n    expect(attr.getValue()).toBe(500);\n    expect(Film.limit).toBe(500);\n\n    Film.limit = 1000;\n\n    expect(attr.getValue()).toBe(1000);\n    expect(Film.limit).toBe(1000);\n    expect(Movie.limit).toBe(500);\n\n    attr = Film.getAttribute('token');\n\n    expect(isAttributeInstance(attr)).toBe(true);\n    expect(attr.getName()).toBe('token');\n    expect(attr.getParent()).toBe(Film);\n    expect(attr.getValue()).toBe('');\n    expect(Film.token).toBe('');\n\n    const film = new Film();\n\n    attr = film.getAttribute('title');\n\n    expect(isAttributeInstance(attr)).toBe(true);\n    expect(attr.getName()).toBe('title');\n    expect(attr.getParent()).toBe(film);\n    expect(typeof attr.getDefault()).toBe('function');\n    expect(attr.evaluateDefault()).toBe('');\n    expect(attr.getValue()).toBe('');\n    expect(film.title).toBe('');\n\n    film.title = 'Léon';\n\n    expect(attr.getValue()).toBe('Léon');\n    expect(film.title).toBe('Léon');\n\n    attr = film.getAttribute('country');\n\n    expect(isAttributeInstance(attr)).toBe(true);\n    expect(attr.getName()).toBe('country');\n    expect(attr.getParent()).toBe(film);\n    expect(typeof attr.getDefault()).toBe('function');\n    expect(attr.evaluateDefault()).toBe('');\n    expect(attr.getValue()).toBe('');\n    expect(film.country).toBe('');\n\n    // --- Using getters ---\n\n    class MotionPicture extends Component {\n      @attribute({getter: () => 100}) static limit: number;\n\n      @attribute({getter: () => 'Untitled'}) title!: string;\n    }\n\n    expect(MotionPicture.limit).toBe(100);\n    expect(MotionPicture.prototype.title).toBe('Untitled');\n\n    expect(() => {\n      class MotionPicture extends Component {\n        @attribute({getter: () => 100}) static limit: number = 30;\n      }\n\n      return MotionPicture;\n    }).toThrow(\n      \"An attribute cannot have both a getter or setter and an initial value (attribute: 'MotionPicture.limit')\"\n    );\n\n    expect(() => {\n      class MotionPicture extends Component {\n        @attribute({getter: () => 'Untitled'}) title: string = '';\n      }\n\n      return MotionPicture;\n    }).toThrow(\n      \"An attribute cannot have both a getter or setter and a default value (attribute: 'MotionPicture.prototype.title')\"\n    );\n  });\n\n  test('@primaryIdentifier()', async () => {\n    class Movie1 extends Component {\n      @primaryIdentifier() id!: string;\n    }\n\n    let idAttribute = Movie1.prototype.getPrimaryIdentifierAttribute();\n\n    expect(isPrimaryIdentifierAttributeInstance(idAttribute)).toBe(true);\n    expect(idAttribute.getName()).toBe('id');\n    expect(idAttribute.getParent()).toBe(Movie1.prototype);\n    expect(isStringValueTypeInstance(idAttribute.getValueType())).toBe(true);\n    expect(typeof idAttribute.getDefault()).toBe('function');\n\n    class Movie2 extends Component {\n      @primaryIdentifier('number') id!: number;\n    }\n\n    idAttribute = Movie2.prototype.getPrimaryIdentifierAttribute();\n\n    expect(isPrimaryIdentifierAttributeInstance(idAttribute)).toBe(true);\n    expect(idAttribute.getName()).toBe('id');\n    expect(idAttribute.getParent()).toBe(Movie2.prototype);\n    expect(isNumberValueTypeInstance(idAttribute.getValueType())).toBe(true);\n    expect(idAttribute.getDefault()).toBeUndefined();\n\n    class Movie3 extends Component {\n      @primaryIdentifier('number') id = Math.random();\n    }\n\n    idAttribute = Movie3.prototype.getPrimaryIdentifierAttribute();\n\n    expect(isPrimaryIdentifierAttributeInstance(idAttribute)).toBe(true);\n    expect(idAttribute.getName()).toBe('id');\n    expect(idAttribute.getParent()).toBe(Movie3.prototype);\n    expect(isNumberValueTypeInstance(idAttribute.getValueType())).toBe(true);\n    expect(typeof idAttribute.getDefault()).toBe('function');\n\n    const movie = new Movie3();\n\n    expect(typeof movie.id === 'number').toBe(true);\n\n    expect(() => {\n      class Movie extends Component {\n        @primaryIdentifier() static id: string;\n      }\n\n      return Movie;\n    }).toThrow(\n      \"Couldn't find a property class while executing @primaryIdentifier() (component: 'Movie', property: 'id')\"\n    );\n\n    expect(() => {\n      class Movie {\n        @primaryIdentifier() id!: string;\n      }\n\n      return Movie;\n    }).toThrow(\"@primaryIdentifier() must be used inside a component class (property: 'id')\");\n\n    expect(() => {\n      class Movie extends Component {\n        @primaryIdentifier() id!: string;\n        @primaryIdentifier() slug!: string;\n      }\n\n      return Movie;\n    }).toThrow(\"The component 'Movie' already has a primary identifier attribute\");\n  });\n\n  test('@secondaryIdentifier()', async () => {\n    class User extends Component {\n      @secondaryIdentifier() email!: string;\n      @secondaryIdentifier() username!: string;\n    }\n\n    const emailAttribute = User.prototype.getSecondaryIdentifierAttribute('email');\n\n    expect(isSecondaryIdentifierAttributeInstance(emailAttribute)).toBe(true);\n    expect(emailAttribute.getName()).toBe('email');\n    expect(emailAttribute.getParent()).toBe(User.prototype);\n    expect(isStringValueTypeInstance(emailAttribute.getValueType())).toBe(true);\n    expect(emailAttribute.getDefault()).toBeUndefined();\n\n    const usernameAttribute = User.prototype.getSecondaryIdentifierAttribute('username');\n\n    expect(isSecondaryIdentifierAttributeInstance(usernameAttribute)).toBe(true);\n    expect(usernameAttribute.getName()).toBe('username');\n    expect(usernameAttribute.getParent()).toBe(User.prototype);\n    expect(isStringValueTypeInstance(usernameAttribute.getValueType())).toBe(true);\n    expect(usernameAttribute.getDefault()).toBeUndefined();\n  });\n\n  test('@method()', async () => {\n    class Movie extends Component {\n      @method() static find() {}\n\n      @method() load() {}\n    }\n\n    expect(typeof Movie.find).toBe('function');\n\n    const movie = new Movie();\n\n    expect(typeof movie.load).toBe('function');\n\n    let meth = Movie.getMethod('find');\n\n    expect(isMethodInstance(meth)).toBe(true);\n    expect(meth.getName()).toBe('find');\n    expect(meth.getParent()).toBe(Movie);\n\n    meth = movie.getMethod('load');\n\n    expect(isMethodInstance(meth)).toBe(true);\n    expect(meth.getName()).toBe('load');\n    expect(meth.getParent()).toBe(movie);\n\n    expect(Movie.hasMethod('delete')).toBe(false);\n    expect(() => Movie.getMethod('delete')).toThrow(\n      \"The method 'delete' is missing (component: 'Movie')\"\n    );\n  });\n\n  test('@expose()', async () => {\n    const testExposure = (componentProvider: () => typeof Component) => {\n      const component = componentProvider();\n\n      let prop = component.getProperty('limit');\n\n      expect(isAttributeInstance(prop)).toBe(true);\n      expect(prop.getName()).toBe('limit');\n      expect(prop.getExposure()).toStrictEqual({get: true});\n\n      prop = component.getProperty('find');\n\n      expect(isMethodInstance(prop)).toBe(true);\n      expect(prop.getName()).toBe('find');\n      expect(prop.getExposure()).toStrictEqual({call: true});\n\n      prop = component.prototype.getProperty('title');\n\n      expect(isAttributeInstance(prop)).toBe(true);\n      expect(prop.getName()).toBe('title');\n      expect(prop.getExposure()).toStrictEqual({get: true, set: true});\n\n      prop = component.prototype.getProperty('load');\n\n      expect(isMethodInstance(prop)).toBe(true);\n      expect(prop.getName()).toBe('load');\n      expect(prop.getExposure()).toStrictEqual({call: true});\n    };\n\n    testExposure(() => {\n      class Movie extends Component {\n        @expose({get: true}) @attribute() static limit: string;\n        @expose({call: true}) @method() static find() {}\n\n        @expose({get: true, set: true}) @attribute() title!: string;\n        @expose({call: true}) @method() load() {}\n      }\n\n      return Movie;\n    });\n\n    testExposure(() => {\n      @expose({\n        limit: {get: true},\n        find: {call: true},\n        prototype: {\n          title: {get: true, set: true},\n          load: {call: true}\n        }\n      })\n      class Movie extends Component {\n        @attribute() static limit: string;\n        @method() static find() {}\n\n        @attribute() title!: string;\n        @method() load() {}\n      }\n\n      return Movie;\n    });\n\n    testExposure(() => {\n      class Movie extends Component {\n        @attribute() static limit: string;\n        @method() static find() {}\n\n        @attribute() title!: string;\n        @method() load() {}\n      }\n\n      @expose({\n        limit: {get: true},\n        find: {call: true},\n        prototype: {\n          title: {get: true, set: true},\n          load: {call: true}\n        }\n      })\n      class ExposedMovie extends Movie {}\n\n      return ExposedMovie;\n    });\n  });\n\n  test('@provide()', async () => {\n    class Movie extends Component {}\n\n    class Backend extends Component {\n      @provide() static Movie = Movie;\n    }\n\n    expect(Backend.getProvidedComponent('Movie')).toBe(Movie);\n\n    ((Backend, BackendMovie) => {\n      class Movie extends BackendMovie {}\n\n      class Frontend extends Backend {\n        @provide() static Movie = Movie;\n      }\n\n      expect(Frontend.getProvidedComponent('Movie')).toBe(Movie);\n    })(Backend, Movie);\n\n    // The backend should not be affected by the frontend\n    expect(Backend.getProvidedComponent('Movie')).toBe(Movie);\n\n    expect(() => {\n      class Movie extends Component {}\n\n      class Backend extends Component {\n        // @ts-expect-error\n        @provide() Movie = Movie;\n      }\n\n      return Backend;\n    }).toThrow(\n      \"@provide() must be used inside a component class with as static attribute declaration (attribute: 'Movie')\"\n    );\n\n    expect(() => {\n      class Movie {}\n\n      class Backend extends Component {\n        @provide() static Movie = Movie;\n      }\n\n      return Backend;\n    }).toThrow(\n      \"@provide() must be used with an attribute declaration specifying a component class (attribute: 'Movie')\"\n    );\n  });\n\n  test('@consume()', async () => {\n    class Movie extends Component {\n      @consume() static Director: typeof Director;\n    }\n\n    class Director extends Component {\n      @consume() static Movie: typeof Movie;\n    }\n\n    class Backend extends Component {\n      @provide() static Movie = Movie;\n      @provide() static Director = Director;\n    }\n\n    expect(Movie.getConsumedComponent('Director')).toBe(Director);\n    expect(Movie.Director).toBe(Director);\n    expect(Director.getConsumedComponent('Movie')).toBe(Movie);\n    expect(Director.Movie).toBe(Movie);\n\n    ((Backend, BackendMovie, BackendDirector) => {\n      class Movie extends BackendMovie {\n        @consume() static Director: typeof Director;\n      }\n\n      class Director extends BackendDirector {\n        @consume() static Movie: typeof Movie;\n      }\n\n      class Frontend extends Backend {\n        @provide() static Movie = Movie;\n        @provide() static Director = Director;\n      }\n\n      expect(Movie.getConsumedComponent('Director')).toBe(Director);\n      expect(Movie.Director).toBe(Director);\n      expect(Director.getConsumedComponent('Movie')).toBe(Movie);\n      expect(Director.Movie).toBe(Movie);\n\n      return Frontend;\n    })(Backend, Movie, Director);\n\n    // The backend should not be affected by the frontend\n    expect(Movie.getConsumedComponent('Director')).toBe(Director);\n    expect(Movie.Director).toBe(Director);\n    expect(Director.getConsumedComponent('Movie')).toBe(Movie);\n    expect(Director.Movie).toBe(Movie);\n\n    expect(() => {\n      class Movie extends Component {\n        // @ts-expect-error\n        @consume() Director: typeof Director;\n      }\n\n      class Director extends Component {}\n\n      return Movie;\n    }).toThrow(\n      \"@consume() must be used inside a component class with as static attribute declaration (attribute: 'Director')\"\n    );\n\n    expect(() => {\n      class Director extends Component {}\n\n      class Movie extends Component {\n        @consume() static Director = Director;\n      }\n\n      return Movie;\n    }).toThrow(\n      \"@consume() must be used with an attribute declaration which does not specify any value (attribute: 'Director')\"\n    );\n  });\n});\n"
  },
  {
    "path": "packages/component/src/decorators.ts",
    "content": "import {hasOwnProperty, getPropertyDescriptor} from 'core-helpers';\n\nimport {Component} from './component';\nimport {\n  Property,\n  Attribute,\n  AttributeOptions,\n  PrimaryIdentifierAttribute,\n  SecondaryIdentifierAttribute,\n  Method,\n  MethodOptions,\n  PropertyExposure\n} from './properties';\nimport {isComponentClassOrInstance, isComponentClass, isComponentInstance} from './utilities';\nimport {\n  getConstructorSourceCode,\n  getAttributeInitializerFromConstructorSourceCode\n} from './js-parser';\n\ntype AttributeDecoratorOptions = Omit<AttributeOptions, 'value' | 'default'>;\n\n/**\n * Decorates an attribute of a component so it can be type checked at runtime, validated, serialized, observed, etc.\n *\n * @param [valueType] A string specifying the [type of values](https://layrjs.com/docs/v2/reference/value-type#supported-types) that can be stored in the attribute (default: `'any'`).\n * @param [options] The options to create the [`Attribute`](https://layrjs.com/docs/v2/reference/attribute#constructor).\n *\n * @example\n * ```\n * // JS\n *\n * import {Component, attribute, validators} from '﹫layr/component';\n *\n * const {maxLength} = validators;\n *\n * class Movie extends Component {\n *   // Optional 'string' class attribute\n *   ﹫attribute('string?') static customName;\n *\n *   // Required 'string' instance attribute\n *   ﹫attribute('string') title;\n *\n *   // Optional 'string' instance attribute with a validator\n *   ﹫attribute('string?', {validators: [maxLength(100)]}) summary;\n *\n *   // Required array of 'Actor' instance attribute with a default value\n *   ﹫attribute('Actor[]') actors = [];\n * }\n * ```\n *\n * @example\n * ```\n * // TS\n *\n * import {Component, attribute, validators} from '﹫layr/component';\n *\n * const {maxLength} = validators;\n *\n * class Movie extends Component {\n *   // Optional 'string' class attribute\n *   ﹫attribute('string?') static customName?: string;\n *\n *   // Required 'string' instance attribute\n *   ﹫attribute('string') title!: string;\n *\n *   // Optional 'string' instance attribute with a validator\n *   ﹫attribute('string?', {validators: [maxLength(100)]}) summary?: string;\n *\n *   // Required array of 'Actor' instance attribute with a default value\n *   ﹫attribute('Actor[]') actors: Actor[] = [];\n * }\n * ```\n *\n * @category Decorators\n * @decorator\n */\nexport function attribute(\n  valueType?: string,\n  options?: AttributeDecoratorOptions\n): PropertyDecorator;\nexport function attribute(options?: AttributeDecoratorOptions): PropertyDecorator;\nexport function attribute(\n  valueType?: string | AttributeDecoratorOptions,\n  options?: AttributeDecoratorOptions\n) {\n  return createAttributeDecorator(\n    new Map([[isComponentClassOrInstance, Attribute]]),\n    'attribute',\n    valueType,\n    options\n  );\n}\n\n/**\n * Decorates an attribute of a component as a [primary identifier attribute](https://layrjs.com/docs/v2/reference/primary-identifier-attribute).\n *\n * @param [valueType] A string specifying the type of values the attribute can store. It can be either `'string'` or `'number'` (default: `'string'`).\n * @param [options] The options to create the [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute).\n *\n * @example\n * ```\n * // JS\n *\n * import {Component, primaryIdentifier} from '﹫layr/component';\n *\n * class Movie extends Component {\n *   // Auto-generated 'string' primary identifier attribute\n *   ﹫primaryIdentifier('string') id;\n * }\n\n * class Film extends Component {\n *   // Custom 'number' primary identifier attribute\n *   ﹫primaryIdentifier('number', {default() { return Math.random(); }}) id;\n * }\n * ```\n *\n * @example\n * ```\n * // TS\n *\n * import {Component, primaryIdentifier} from '﹫layr/component';\n *\n * class Movie extends Component {\n *   // Auto-generated 'string' primary identifier attribute\n *   ﹫primaryIdentifier('string') id!: string;\n * }\n\n * class Film extends Component {\n *   // Custom 'number' primary identifier attribute\n *   ﹫primaryIdentifier('number', {default() { return Math.random(); }}) id!: number;\n * }\n * ```\n *\n * @category Decorators\n * @decorator\n */\nexport function primaryIdentifier(\n  valueType?: string,\n  options?: AttributeDecoratorOptions\n): PropertyDecorator;\nexport function primaryIdentifier(options?: AttributeDecoratorOptions): PropertyDecorator;\nexport function primaryIdentifier(\n  valueType?: string | AttributeDecoratorOptions,\n  options?: AttributeDecoratorOptions\n) {\n  return createAttributeDecorator(\n    new Map([[isComponentInstance, PrimaryIdentifierAttribute]]),\n    'primaryIdentifier',\n    valueType,\n    options\n  );\n}\n\n/**\n * Decorates an attribute of a component as a [secondary identifier attribute](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute).\n *\n * @param [valueType] A string specifying the type of values the attribute can store. It can be either `'string'` or `'number'` (default: `'string'`).\n * @param [options] The options to create the [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute).\n *\n * @example\n * ```\n * // JS\n *\n * import {Component, secondaryIdentifier} from '﹫layr/component';\n *\n * class Movie extends Component {\n *   // 'string' secondary identifier attribute\n *   ﹫secondaryIdentifier('string') slug;\n *\n *   // 'number' secondary identifier attribute\n *   ﹫secondaryIdentifier('number') reference;\n * }\n * ```\n *\n * @example\n * ```\n * // TS\n *\n * import {Component, secondaryIdentifier} from '﹫layr/component';\n *\n * class Movie extends Component {\n *   // 'string' secondary identifier attribute\n *   ﹫secondaryIdentifier('string') slug!: string;\n *\n *   // 'number' secondary identifier attribute\n *   ﹫secondaryIdentifier('number') reference!: number;\n * }\n * ```\n *\n * @category Decorators\n * @decorator\n */\nexport function secondaryIdentifier(\n  valueType?: string,\n  options?: AttributeDecoratorOptions\n): PropertyDecorator;\nexport function secondaryIdentifier(options?: AttributeDecoratorOptions): PropertyDecorator;\nexport function secondaryIdentifier(\n  valueType?: string | AttributeDecoratorOptions,\n  options?: AttributeDecoratorOptions\n) {\n  return createAttributeDecorator(\n    new Map([[isComponentInstance, SecondaryIdentifierAttribute]]),\n    'secondaryIdentifier',\n    valueType,\n    options\n  );\n}\n\nexport function createAttributeDecorator(\n  AttributeClassMap: PropertyClassMap,\n  decoratorName: string,\n  valueType?: string | AttributeDecoratorOptions,\n  options: AttributeDecoratorOptions = {}\n) {\n  if (typeof valueType === 'string') {\n    options = {...options, valueType};\n  } else if (valueType !== undefined) {\n    options = valueType;\n  }\n\n  if ('value' in options || 'default' in options) {\n    throw new Error(`The options 'value' and 'default' are not authorized in @${decoratorName}()`);\n  }\n\n  let attributeOptions: AttributeOptions = options;\n\n  return function (\n    target: typeof Component | Component,\n    name: string,\n    descriptor?: PropertyDescriptor\n  ) {\n    if (!isComponentClassOrInstance(target)) {\n      throw new Error(\n        `@${decoratorName}() must be used inside a component class (property: '${name}')`\n      );\n    }\n\n    if (isComponentClass(target)) {\n      const value =\n        !target.hasAttribute(name) || target.getAttribute(name).isSet()\n          ? (target as any)[name]\n          : undefined;\n      attributeOptions = {value, ...attributeOptions};\n    } else {\n      const initializer = getAttributeInitializer(target, name, descriptor);\n      if (initializer !== undefined) {\n        attributeOptions = {default: initializer, ...attributeOptions};\n      }\n    }\n\n    const AttributeClass = getPropertyClass(AttributeClassMap, target, {\n      decoratorName,\n      propertyName: name\n    });\n\n    const attribute = target.setProperty(name, AttributeClass, attributeOptions) as Attribute;\n\n    const compiler = determineCompiler(descriptor);\n\n    if (compiler === 'typescript' && 'default' in attributeOptions) {\n      if (attribute._isDefaultSetInConstructor) {\n        throw new Error(\n          `Cannot set a default value to an attribute that already has an inherited default value (property: '${name}')`\n        );\n      }\n\n      attribute._isDefaultSetInConstructor = true;\n    }\n\n    if (compiler === 'babel-legacy') {\n      return getPropertyDescriptor(target, name) as void;\n    }\n  };\n}\n\nfunction getAttributeInitializer(\n  component: Component,\n  attributeName: string,\n  descriptor?: PropertyDescriptor & {initializer?: any}\n) {\n  if (determineCompiler(descriptor) === 'babel-legacy') {\n    return typeof descriptor!.initializer === 'function' ? descriptor!.initializer : undefined;\n  }\n\n  if (!hasOwnProperty(component, '__constructorSourceCode')) {\n    const classSourceCode = component.constructor.toString();\n    const constructorSourceCode = getConstructorSourceCode(classSourceCode);\n    Object.defineProperty(component, '__constructorSourceCode', {value: constructorSourceCode});\n  }\n\n  const constructorSourceCode = component.__constructorSourceCode;\n\n  if (constructorSourceCode === undefined) {\n    return undefined;\n  }\n\n  return getAttributeInitializerFromConstructorSourceCode(constructorSourceCode, attributeName);\n}\n\n/**\n * Decorates a method of a component so it can be exposed and called remotely.\n *\n * @param [options] The options to create the [`Method`](https://layrjs.com/docs/v2/reference/method#constructor).\n *\n * @example\n * ```\n * import {Component, method} from '﹫layr/component';\n *\n * class Movie extends Component {\n *   // Class method\n *   ﹫method() static getConfig() {\n *     // ...\n *   }\n *\n *   // Instance method\n *   ﹫method() play() {\n *     // ...\n *   }\n * }\n * ```\n *\n * @category Decorators\n * @decorator\n */\nexport function method(options: MethodOptions = {}) {\n  return createMethodDecorator(new Map([[isComponentClassOrInstance, Method]]), 'method', options);\n}\n\nexport function createMethodDecorator(\n  MethodClassMap: PropertyClassMap,\n  decoratorName: string,\n  options: MethodOptions = {}\n) {\n  return function (\n    target: typeof Component | Component,\n    name: string,\n    descriptor: PropertyDescriptor\n  ) {\n    if (!isComponentClassOrInstance(target)) {\n      throw new Error(\n        `@${decoratorName}() must be used inside a component class (property: '${name}')`\n      );\n    }\n\n    if (!(typeof descriptor.value === 'function' && descriptor.enumerable === false)) {\n      throw new Error(\n        `@${decoratorName}() must be used with a method declaration (property: '${name}')`\n      );\n    }\n\n    const MethodClass = getPropertyClass(MethodClassMap, target, {\n      decoratorName,\n      propertyName: name\n    });\n\n    target.setProperty(name, MethodClass, options);\n  };\n}\n\ntype PropertyClassMap = Map<(value: any) => boolean, typeof Property>;\n\nfunction getPropertyClass(\n  propertyClassMap: PropertyClassMap,\n  target: typeof Component | Component,\n  {decoratorName, propertyName}: {decoratorName: string; propertyName: string}\n) {\n  for (const [func, propertyClass] of propertyClassMap.entries()) {\n    if (func(target)) {\n      return propertyClass;\n    }\n  }\n\n  throw new Error(\n    `Couldn't find a property class while executing @${decoratorName}() (${target.describeComponent()}, property: '${propertyName}')`\n  );\n}\n\ntype ClassExposure = {\n  [name: string]: PropertyExposure | {[name: string]: PropertyExposure};\n};\n\n/**\n * Exposes some attributes or methods of a component so they can be consumed remotely.\n *\n * This decorator is usually placed before a component attribute or method, but it can also be placed before a component class. When placed before a component class, you can expose several attributes or methods at once, and even better, you can expose attributes or methods that are defined in a parent class.\n *\n * @param exposure An object specifying which operations should be exposed. When the decorator is placed before a component attribute or method, the object is of type [`PropertyExposure`](https://layrjs.com/docs/v2/reference/property#property-exposure-type). When the decorator is placed before a component class, the shape of the object is `{[propertyName]: PropertyExposure, prototype: {[propertyName]: PropertyExposure}}`.\n *\n * @example\n * ```\n * // JS\n *\n * import {Component, expose, attribute, method} from '﹫layr/component';\n *\n * class Movie extends Component {\n *   // Class attribute exposing the 'get' operation only\n *   ﹫expose({get: true}) ﹫attribute('string?') static customName;\n *\n *   // Instance attribute exposing the 'get' and 'set' operations\n *   ﹫expose({get: true, set: true}) ﹫attribute('string') title;\n *\n *   // Class method exposure\n *   ﹫expose({call: true}) ﹫method() static getConfig() {\n *     // ...\n *   }\n *\n *   // Instance method exposure\n *   ﹫expose({call: true}) ﹫method() play() {\n *     // ...\n *   }\n * }\n *\n * // Exposing some class and instance methods that are defined in a parent class\n * ﹫expose({find: {call: true}, prototype: {load: {call: true}}})\n * class Actor extends Storable(Component) {\n *   // ...\n * }\n * ```\n *\n * @example\n * ```\n * // TS\n *\n * import {Component, expose, attribute, method} from '﹫layr/component';\n *\n * class Movie extends Component {\n *   // Class attribute exposing the 'get' operation only\n *   ﹫expose({get: true}) ﹫attribute('string?') static customName?: string;\n *\n *   // Instance attribute exposing the 'get' and 'set' operations\n *   ﹫expose({get: true, set: true}) ﹫attribute('string') title!: string;\n *\n *   // Class method exposure\n *   ﹫expose({call: true}) ﹫method() static getConfig() {\n *     // ...\n *   }\n *\n *   // Instance method exposure\n *   ﹫expose({call: true}) ﹫method() play() {\n *     // ...\n *   }\n * }\n *\n * // Exposing some class and instance methods that are defined in a parent class\n * ﹫expose({find: {call: true}, prototype: {load: {call: true}}})\n * class Actor extends Storable(Component) {\n *   // ...\n * }\n * ```\n *\n * @category Decorators\n * @decorator\n */\nexport function expose(exposure: ClassExposure): (target: typeof Component | Component) => void;\nexport function expose(\n  exposure: PropertyExposure\n): (target: typeof Component | Component, name: string) => void;\nexport function expose(exposure: ClassExposure | PropertyExposure = {}) {\n  return function (target: typeof Component | Component, name?: string) {\n    if (name === undefined) {\n      // Class decorator\n\n      if (!isComponentClass(target)) {\n        throw new Error(\n          `@expose() must be used as a component class decorator or a component property decorator`\n        );\n      }\n\n      const _expose = (\n        target: typeof Component | Component,\n        exposures: {[name: string]: PropertyExposure}\n      ) => {\n        for (const [name, exposure] of Object.entries(exposures)) {\n          target.getProperty(name).setExposure(exposure);\n        }\n      };\n\n      const {prototype: prototypeExposure, ...classExposure} = exposure as ClassExposure;\n\n      _expose(target, classExposure);\n\n      if (prototypeExposure !== undefined) {\n        _expose(target.prototype, prototypeExposure as {[name: string]: PropertyExposure});\n      }\n\n      return;\n    }\n\n    // Property decorator\n\n    if (!isComponentClassOrInstance(target)) {\n      throw new Error(\n        `@expose() must be as a component class decorator or a component property decorator (property: '${name}')`\n      );\n    }\n\n    if (\n      !target.hasProperty(name) ||\n      target.getProperty(name, {autoFork: false}).getParent() !== target\n    ) {\n      throw new Error(\n        `@expose() must be used in combination with @attribute() or @method() (property: '${name}')`\n      );\n    }\n\n    target.getProperty(name).setExposure(exposure);\n  };\n}\n\n/**\n * Provides a component so it can be easily accessed from the current component or from any component that is \"consuming\" it using the [`@consume()`](https://layrjs.com/docs/v2/reference/component#consume-decorator) decorator.\n *\n * @example\n * ```\n * // JS\n *\n * import {Component, provide, consume} from '﹫layr/component';\n *\n * class Movie extends Component {\n *   ﹫consume() static Actor;\n * }\n *\n * class Actor extends Component {}\n *\n * class Application extends Component {\n *   ﹫provide() static Movie = Movie;\n *   ﹫provide() static Actor = Actor;\n * }\n *\n * // Since `Actor` is provided by `Application`, it can be accessed from `Movie`\n * Movie.Actor; // => Actor\n * ```\n *\n * @example\n * ```\n * // TS\n *\n * import {Component, provide, consume} from '﹫layr/component';\n *\n * class Movie extends Component {\n *   ﹫consume() static Actor: typeof Actor;\n * }\n *\n * class Actor extends Component {}\n *\n * class Application extends Component {\n *   ﹫provide() static Movie = Movie;\n *   ﹫provide() static Actor = Actor;\n * }\n *\n * // Since `Actor` is provided by `Application`, it can be accessed from `Movie`\n * Movie.Actor; // => Actor\n * ```\n *\n * @category Decorators\n * @decorator\n */\nexport function provide() {\n  return function (target: typeof Component, name: string, descriptor?: PropertyDescriptor) {\n    if (!isComponentClass(target)) {\n      throw new Error(\n        `@provide() must be used inside a component class with as static attribute declaration (attribute: '${name}')`\n      );\n    }\n\n    const compiler = determineCompiler(descriptor);\n\n    const component = Object.getOwnPropertyDescriptor(target, name)?.value;\n\n    if (!isComponentClass(component)) {\n      throw new Error(\n        `@provide() must be used with an attribute declaration specifying a component class (attribute: '${name}')`\n      );\n    }\n\n    target.provideComponent(component);\n\n    if (compiler === 'babel-legacy') {\n      return getPropertyDescriptor(target, name) as void;\n    }\n  };\n}\n\n/**\n * Consumes a component provided by the provider (or recursively, any provider's provider) of the current component so it can be easily accessed using a component accessor.\n *\n * @examplelink See [`@provide()`'s example](https://layrjs.com/docs/v2/reference/component#provide-decorator).\n *\n * @category Decorators\n * @decorator\n */\nexport function consume() {\n  return function (target: typeof Component, name: string, descriptor?: PropertyDescriptor) {\n    if (!isComponentClass(target)) {\n      throw new Error(\n        `@consume() must be used inside a component class with as static attribute declaration (attribute: '${name}')`\n      );\n    }\n\n    const compiler = determineCompiler(descriptor);\n\n    if (hasOwnProperty(target, name)) {\n      const propertyValue = (target as any)[name];\n\n      if (propertyValue !== undefined) {\n        throw new Error(\n          `@consume() must be used with an attribute declaration which does not specify any value (attribute: '${name}')`\n        );\n      }\n\n      if (compiler === 'babel-legacy') {\n        delete (target as any)[name];\n      }\n    }\n\n    target.consumeComponent(name);\n\n    if (compiler === 'babel-legacy') {\n      return getPropertyDescriptor(target, name) as void;\n    }\n  };\n}\n\nexport function determineCompiler(descriptor: PropertyDescriptor | undefined) {\n  if (typeof descriptor === 'object') {\n    // The class has been compiled by Babel using @babel/plugin-proposal-decorators in legacy mode\n    return 'babel-legacy';\n  } else {\n    // The class has been compiled by the TypeScript compiler\n    return 'typescript';\n  }\n}\n"
  },
  {
    "path": "packages/component/src/deserialization.test.ts",
    "content": "import {Component} from './component';\nimport {EmbeddedComponent} from './embedded-component';\nimport {provide, attribute, primaryIdentifier} from './decorators';\nimport {deserialize} from './deserialization';\n\ndescribe('Deserialization', () => {\n  test('Component classes', async () => {\n    class Movie extends Component {\n      @attribute() static limit = 100;\n      @attribute() static offset: number;\n    }\n\n    class Application extends Component {\n      @provide() static Movie = Movie;\n    }\n\n    expect(Movie.limit).toBe(100);\n    expect(Movie.offset).toBeUndefined();\n\n    // --- Using the deserialize() function ---\n\n    let DeserializedMovie = deserialize(\n      {\n        __component: 'typeof Movie',\n        limit: {__undefined: true},\n        offset: 30\n      },\n      {rootComponent: Application}\n    );\n\n    expect(DeserializedMovie).toBe(Movie);\n    expect(Movie.limit).toBeUndefined();\n    expect(Movie.offset).toBe(30);\n\n    DeserializedMovie = deserialize(\n      {__component: 'typeof Movie', limit: 1000, offset: {__undefined: true}},\n      {rootComponent: Application}\n    );\n\n    expect(DeserializedMovie).toBe(Movie);\n    expect(Movie.limit).toBe(1000);\n    expect(Movie.offset).toBeUndefined();\n\n    DeserializedMovie = deserialize({__component: 'typeof Movie'}, {rootComponent: Application});\n\n    expect(DeserializedMovie).toBe(Movie);\n    expect(Movie.limit).toBe(1000);\n    expect(Movie.offset).toBeUndefined();\n\n    expect(() => deserialize({__component: 'typeof Film'}, {rootComponent: Application})).toThrow(\n      \"Cannot get the component of type 'typeof Film' from the component 'Application'\"\n    );\n\n    expect(() => deserialize({__component: 'typeof Movie'})).toThrow(\n      \"Cannot deserialize a component when no 'rootComponent' is provided\"\n    );\n\n    // --- Using the deserialize() function with the 'source' option ---\n\n    expect(Movie.limit).toBe(1000);\n    expect(Movie.getAttribute('limit').getValueSource()).toBe('local');\n\n    DeserializedMovie = deserialize(\n      {__component: 'typeof Movie', limit: 5000},\n      {source: 'server', rootComponent: Application}\n    );\n\n    expect(Movie.limit).toBe(5000);\n    expect(Movie.getAttribute('limit').getValueSource()).toBe('server');\n\n    DeserializedMovie = deserialize(\n      {__component: 'typeof Movie', limit: 5000},\n      {rootComponent: Application}\n    );\n\n    expect(Movie.limit).toBe(5000);\n    expect(Movie.getAttribute('limit').getValueSource()).toBe('local');\n\n    // --- Using Component.deserialize() method ---\n\n    DeserializedMovie = Movie.deserialize({limit: {__undefined: true}, offset: 100});\n\n    expect(DeserializedMovie).toBe(Movie);\n    expect(Movie.limit).toBeUndefined();\n    expect(Movie.offset).toBe(100);\n\n    expect(() => Movie.deserialize({__component: 'typeof Film'})).toThrow(\n      \"An unexpected component type was encountered while deserializing an object (encountered type: 'typeof Film', expected type: 'typeof Movie')\"\n    );\n  });\n\n  test('Component instances', async () => {\n    class Movie extends Component {\n      @attribute() title?: string;\n      @attribute() duration = 0;\n    }\n\n    class Application extends Component {\n      @provide() static Movie = Movie;\n    }\n\n    // --- Using the deserialize() function ---\n\n    let movie = deserialize(\n      {__component: 'Movie', title: 'Inception'},\n      {rootComponent: Application}\n    ) as Movie;\n\n    expect(movie).toBeInstanceOf(Movie);\n    expect(movie).not.toBe(Movie.prototype);\n    expect(movie.isNew()).toBe(false);\n    expect(movie.title).toBe('Inception');\n    expect(movie.getAttribute('duration').isSet()).toBe(false);\n\n    movie = deserialize(\n      {__component: 'Movie', __new: true, title: 'Inception'},\n      {rootComponent: Application}\n    ) as Movie;\n\n    expect(movie).toBeInstanceOf(Movie);\n    expect(movie).not.toBe(Movie.prototype);\n    expect(movie.isNew()).toBe(true);\n    expect(movie.title).toBe('Inception');\n    expect(movie.duration).toBe(0);\n\n    movie = deserialize(\n      {__component: 'Movie', __new: true, duration: {__undefined: true}},\n      {rootComponent: Application}\n    ) as Movie;\n\n    expect(movie.title).toBeUndefined();\n    expect(movie.duration).toBeUndefined();\n\n    movie = deserialize(\n      {__component: 'Movie', __new: true, title: 'Inception', duration: 120},\n      {\n        rootComponent: Application,\n        attributeFilter(attribute) {\n          expect(this).toBeInstanceOf(Movie);\n          expect(attribute.getParent()).toBe(this);\n          return attribute.getName() === 'title';\n        }\n      }\n    ) as Movie;\n\n    expect(movie.title).toBe('Inception');\n    expect(movie.duration).toBe(0);\n\n    expect(() => deserialize({__component: 'Film'}, {rootComponent: Application})).toThrow(\n      \"Cannot get the component of type 'Film' from the component 'Application'\"\n    );\n\n    // --- Using the deserialize() function with the 'source' option ---\n\n    movie = deserialize(\n      {__component: 'Movie', title: 'Inception'},\n      {rootComponent: Application}\n    ) as Movie;\n\n    expect(movie.title).toBe('Inception');\n    expect(movie.getAttribute('title').getValueSource()).toBe('local');\n    expect(movie.getAttribute('duration').isSet()).toBe(false);\n\n    movie = deserialize(\n      {__component: 'Movie', title: 'Inception'},\n      {source: 'server', rootComponent: Application}\n    ) as Movie;\n\n    expect(movie.title).toBe('Inception');\n    expect(movie.getAttribute('title').getValueSource()).toBe('server');\n    expect(movie.getAttribute('duration').isSet()).toBe(false);\n\n    movie = deserialize(\n      {__component: 'Movie', __new: true, title: 'Inception'},\n      {source: 'server', rootComponent: Application}\n    ) as Movie;\n\n    expect(movie.title).toBe('Inception');\n    expect(movie.getAttribute('title').getValueSource()).toBe('server');\n    expect(movie.duration).toBe(0);\n    expect(movie.getAttribute('duration').getValueSource()).toBe('local');\n\n    // --- Using component.deserialize() method ---\n\n    movie = new Movie();\n\n    expect(movie.isNew()).toBe(true);\n    expect(movie.title).toBeUndefined();\n    expect(movie.duration).toBe(0);\n\n    const deserializedMovie = movie.deserialize({__new: true, title: 'Inception'});\n\n    expect(deserializedMovie).toBe(movie);\n    expect(movie.isNew()).toBe(true);\n    expect(movie.title).toBe('Inception');\n    expect(movie.duration).toBe(0);\n\n    movie.deserialize({__new: true, duration: 120});\n\n    expect(movie.isNew()).toBe(true);\n    expect(movie.title).toBe('Inception');\n    expect(movie.duration).toBe(120);\n\n    movie.deserialize({});\n\n    expect(movie).toBe(movie);\n    expect(movie.isNew()).toBe(false);\n    expect(movie.title).toBe('Inception');\n    expect(movie.duration).toBe(120);\n\n    expect(() => movie.deserialize({__component: 'Film'})).toThrow(\n      \"An unexpected component type was encountered while deserializing an object (encountered type: 'Film', expected type: 'Movie')\"\n    );\n\n    expect(() => movie.deserialize({__new: true})).toThrow(\n      \"Cannot mark as new an existing non-new component (component: 'Application.Movie')\"\n    );\n  });\n\n  test('Embedded component instances', async () => {\n    class Person extends EmbeddedComponent {\n      @attribute('string') fullName?: string;\n    }\n\n    class Movie extends Component {\n      @provide() static Person = Person;\n\n      @attribute() title?: string;\n      @attribute('Person?') director?: Person;\n      @attribute('Person[]?') actors?: Person[];\n    }\n\n    class Application extends Component {\n      @provide() static Movie = Movie;\n    }\n\n    const movie1 = deserialize(\n      {\n        __component: 'Movie',\n        title: 'Movie 1',\n        director: {__component: 'Person', fullName: 'Person 1'}\n      },\n      {rootComponent: Application}\n    ) as Movie;\n\n    expect(movie1).toBeInstanceOf(Movie);\n    expect(movie1.title).toBe('Movie 1');\n\n    const movie1Director = movie1.director;\n\n    expect(movie1Director).toBeInstanceOf(Person);\n    expect(movie1Director?.fullName).toBe('Person 1');\n\n    movie1.deserialize({director: {__component: 'Person', fullName: 'Person 1 (modified)'}});\n\n    // The identity of movie1.director should be preserved\n    expect(movie1.director).toBe(movie1Director);\n    expect(movie1Director?.fullName).toBe('Person 1 (modified)');\n\n    const movie2 = deserialize(\n      {\n        __component: 'Movie',\n        title: 'Movie 2',\n        actors: [{__component: 'Person', fullName: 'Person 2'}]\n      },\n      {rootComponent: Application}\n    ) as Movie;\n\n    expect(movie2).toBeInstanceOf(Movie);\n    expect(movie2.title).toBe('Movie 2');\n\n    const movie2Actor = movie2.actors![0];\n\n    expect(movie2Actor).toBeInstanceOf(Person);\n    expect(movie2Actor?.fullName).toBe('Person 2');\n\n    movie2.deserialize({actors: [{__component: 'Person', fullName: 'Person 2 (modified)'}]});\n\n    // The identity of movie2.actors[0] should NOT be preserved\n    expect(movie2.actors![0]).not.toBe(movie2Actor);\n    expect(movie2.actors![0].fullName).toBe('Person 2 (modified)');\n  });\n\n  test('Referenced component instances', async () => {\n    class Person extends Component {\n      @primaryIdentifier() id!: string;\n      @attribute('string') fullName?: string;\n    }\n\n    class Movie extends Component {\n      @provide() static Person = Person;\n\n      @primaryIdentifier() id!: string;\n      @attribute() title?: string;\n      @attribute('Person?') director?: Person;\n    }\n\n    class Application extends Component {\n      @provide() static Movie = Movie;\n    }\n\n    const person1 = new Person({id: 'person1', fullName: 'Person 1'});\n    const person2 = new Person({id: 'person2', fullName: 'Person 2'});\n\n    const movie1 = new Movie({id: 'movie1', title: 'Movie 1', director: person1});\n\n    expect(movie1.director).toBe(person1);\n\n    let deserializedMovie = deserialize(\n      {\n        __component: 'Movie',\n        id: 'movie1',\n        director: {__component: 'Person', id: 'person2'}\n      },\n      {rootComponent: Application}\n    ) as Movie;\n\n    expect(deserializedMovie).toBe(movie1);\n    expect(movie1.director).toBe(person2);\n\n    deserializedMovie = deserialize(\n      {\n        __component: 'Movie',\n        id: 'movie1',\n        director: {__undefined: true}\n      },\n      {rootComponent: Application}\n    ) as Movie;\n\n    expect(deserializedMovie).toBe(movie1);\n    expect(movie1.director).toBeUndefined();\n  });\n\n  test('Functions', async () => {\n    let serializedFunction: any = {\n      __function: 'function sum(a, b) { return a + b; }'\n    };\n\n    let func = deserialize(serializedFunction) as Function;\n\n    expect(typeof func).toBe('object');\n    expect(func).toEqual(serializedFunction);\n\n    func = deserialize(serializedFunction, {deserializeFunctions: true}) as Function;\n\n    expect(typeof func).toBe('function');\n    expect(Object.keys(func)).toEqual([]);\n    expect(func.name).toBe('sum');\n    expect(func(1, 2)).toBe(3);\n\n    serializedFunction.displayName = 'sum';\n\n    func = deserialize(serializedFunction) as Function;\n\n    expect(typeof func).toBe('object');\n    expect(func).toEqual(serializedFunction);\n\n    func = deserialize(serializedFunction, {deserializeFunctions: true}) as Function;\n\n    expect(typeof func).toBe('function');\n    expect(func.name).toBe('sum');\n    expect(Object.keys(func)).toEqual(['displayName']);\n    expect((func as any).displayName).toBe('sum');\n    expect(func(1, 2)).toBe(3);\n  });\n});\n"
  },
  {
    "path": "packages/component/src/deserialization.ts",
    "content": "import {\n  deserialize as simpleDeserialize,\n  DeserializeOptions as SimpleDeserializeOptions,\n  DeserializeResult\n} from 'simple-serialization';\nimport {possiblyAsync} from 'possibly-async';\nimport {PlainObject} from 'core-helpers';\n\nimport type {Component, ComponentSet} from './component';\nimport type {PropertyFilter, ValueSource} from './properties';\nimport {Validator, isSerializedValidator} from './validation/validator';\nimport {isComponentClass} from './utilities';\n\nexport type DeserializeOptions = SimpleDeserializeOptions & {\n  rootComponent?: typeof Component;\n  attributeFilter?: PropertyFilter;\n  deserializedComponents?: ComponentSet;\n  deserializeFunctions?: boolean;\n  source?: ValueSource;\n};\n\n/**\n * Deserializes any type of serialized values including objects, arrays, dates, and components.\n *\n * @param value A serialized value.\n * @param [options.rootComponent] The root component of your app.\n * @param [options.attributeFilter] A (possibly async) function used to filter the component attributes to be deserialized. The function is invoked for each attribute with an [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) instance as first argument.\n * @param [options.source] The source of the serialization (default: `'local'`).\n *\n * @returns The deserialized value.\n *\n * @example\n * ```\n * // JS\n *\n * import {Component, deserialize} from '﹫layr/component';\n *\n * class Movie extends Component {\n *   ﹫attribute('string') title;\n * }\n *\n * const serializedData = {\n *   createdOn: {__date: \"2020-07-18T23:43:33.778Z\"},\n *   updatedOn: {__undefined: true},\n *   movie: {__component: 'Movie', title: 'Inception'}\n * };\n *\n * const data = deserialize(serializedData, {rootComponent: Movie});\n *\n * data.createdOn; // => A Date instance\n * data.updatedOn; // => undefined\n * data.movie; // => A Movie instance\n * ```\n *\n * @example\n * ```\n * // TS\n *\n * import {Component, deserialize} from '﹫layr/component';\n *\n * class Movie extends Component {\n *   ﹫attribute('string') title!: string;\n * }\n *\n * const serializedData = {\n *   createdOn: {__date: \"2020-07-18T23:43:33.778Z\"},\n *   updatedOn: {__undefined: true},\n *   movie: {__component: 'Movie', title: 'Inception'}\n * };\n *\n * const data = deserialize(serializedData, {rootComponent: Movie});\n *\n * data.createdOn; // => A Date instance\n * data.updatedOn; // => undefined\n * data.movie; // => A Movie instance\n * ```\n *\n * @category Deserialization\n * @possiblyasync\n */\nexport function deserialize<Value>(\n  value: Value,\n  options?: DeserializeOptions\n): DeserializeResult<Value>;\nexport function deserialize(value: any, options: DeserializeOptions = {}) {\n  const {\n    objectDeserializer: originalObjectDeserializer,\n    functionDeserializer: originalFunctionDeserializer,\n    rootComponent,\n    attributeFilter,\n    deserializedComponents,\n    deserializeFunctions = false,\n    source,\n    ...otherOptions\n  } = options;\n\n  const objectDeserializer = function (object: PlainObject) {\n    if (originalObjectDeserializer !== undefined) {\n      const deserializedObject = originalObjectDeserializer(object);\n\n      if (deserializedObject !== undefined) {\n        return deserializedObject;\n      }\n    }\n\n    if (isSerializedValidator(object)) {\n      return Validator.recreate(object, deserialize);\n    }\n\n    const {\n      __component: componentType,\n      __new: isNew = false,\n      ...attributes\n    }: {__component?: string; __new?: boolean} & Record<string, any> = object;\n\n    if (componentType === undefined) {\n      return undefined;\n    }\n\n    if (rootComponent === undefined) {\n      throw new Error(\"Cannot deserialize a component when no 'rootComponent' is provided\");\n    }\n\n    const componentClassOrPrototype = rootComponent.getComponentOfType(componentType);\n\n    if (isComponentClass(componentClassOrPrototype)) {\n      const componentClass = componentClassOrPrototype;\n\n      return componentClass.deserialize(attributes, options);\n    }\n\n    const componentPrototype = componentClassOrPrototype;\n    const componentClass = componentPrototype.constructor;\n\n    const identifiers = componentPrototype.__createIdentifierSelectorFromObject(attributes);\n\n    const component = componentClass.instantiate(identifiers, {source});\n\n    return possiblyAsync(component, (component) => {\n      component.setIsNewMark(isNew);\n\n      if (deserializedComponents !== undefined && !componentClass.isEmbedded()) {\n        deserializedComponents.add(component);\n      }\n\n      return possiblyAsync(component.__deserializeAttributes(attributes, options), () => {\n        if (isNew) {\n          for (const attribute of component.getAttributes()) {\n            if (!(attribute.isSet() || attribute.isControlled())) {\n              attribute.setValue(attribute.evaluateDefault());\n            }\n          }\n        }\n\n        return component;\n      });\n    });\n  };\n\n  let functionDeserializer: DeserializeOptions['functionDeserializer'];\n\n  if (deserializeFunctions) {\n    functionDeserializer = function (object) {\n      if (originalFunctionDeserializer !== undefined) {\n        const deserializedFunction = originalFunctionDeserializer(object);\n\n        if (deserializedFunction !== undefined) {\n          return deserializedFunction;\n        }\n      }\n\n      const {__function, ...serializedAttributes} = object;\n\n      if (__function === undefined) {\n        return undefined;\n      }\n\n      const functionCode = __function;\n\n      return possiblyAsync(\n        possiblyAsync.mapValues(serializedAttributes, (attributeValue) =>\n          simpleDeserialize(attributeValue, {\n            ...otherOptions,\n            objectDeserializer,\n            functionDeserializer\n          })\n        ),\n        (deserializedAttributes) => {\n          const deserializedFunction = deserializeFunction(functionCode);\n          Object.assign(deserializedFunction, deserializedAttributes);\n          return deserializedFunction;\n        }\n      );\n    };\n  }\n\n  return simpleDeserialize(value, {...otherOptions, objectDeserializer, functionDeserializer});\n}\n\nexport function deserializeFunction(functionCode: string): Function {\n  return new Function(`return (${functionCode});`)();\n\n  // let evalCode = `(${functionCode});`;\n\n  // if (context !== undefined) {\n  //   const contextKeys = Object.keys(context).join(', ');\n  //   const contextCode = `const {${contextKeys}} = context;`;\n  //   evalCode = `${contextCode} ${evalCode}`;\n  // }\n\n  // return eval(evalCode);\n}\n"
  },
  {
    "path": "packages/component/src/embedded-component.ts",
    "content": "import {Component} from './component';\n\n/**\n * *Inherits from [`Component`](https://layrjs.com/docs/v2/reference/component).*\n *\n * The `EmbeddedComponent` class allows you to define a component that can be embedded into another component. This is useful when you have to deal with a rich data model composed of a hierarchy of properties that can be type checked at runtime and validated. If you don't need such control over some nested attributes, instead of using an embedded component, you can just use an attribute of type `object`.\n *\n * The `EmbeddedComponent` class inherits from the [`Component`](https://layrjs.com/docs/v2/reference/component) class, so you can define and consume an embedded component in the same way you would do with any component.\n *\n * However, since an embedded component is owned by its parent component, it doesn't behave like a regular component. Head over [here](https://layrjs.com/docs/v2/reference/component#nesting-components) for a broader explanation.\n *\n * #### Usage\n *\n * Just extend the `EmbeddedComponent` class to define a component that has the ability to be embedded.\n *\n * For example, a `MovieDetails` embedded component could be defined as follows:\n *\n * ```\n * // JS\n *\n * // movie-details.js\n *\n * import {EmbeddedComponent} from '@layr/component';\n *\n * export class MovieDetails extends EmbeddedComponent {\n *   ﹫attribute('number?') duration;\n *   ﹫attribute('string?') aspectRatio;\n * }\n * ```\n *\n * ```\n * // TS\n *\n * // movie-details.ts\n *\n * import {EmbeddedComponent} from '@layr/component';\n *\n * export class MovieDetails extends EmbeddedComponent {\n *   ﹫attribute('number?') duration?: number;\n *   ﹫attribute('string?') aspectRatio?: string;\n * }\n * ```\n *\n * Once you have defined an embedded component, you can embed it into any other component (a regular component or even another embedded component). For example, here is a `Movie` component that is embedding the `MovieDetails` component:\n *\n * ```\n * // JS\n *\n * // movie.js\n *\n * import {Component} from '@layr/component';\n *\n * import {MovieDetails} from './movie-details';\n *\n * class Movie extends Component {\n *   ﹫provide() static MovieDetails = MovieDetails;\n *\n *   ﹫attribute('string') title;\n *   ﹫attribute('MovieDetails') details;\n * }\n * ```\n *\n * ```\n * // TS\n *\n * // movie.ts\n *\n * import {Component} from '@layr/component';\n *\n * import {MovieDetails} from './movie-details';\n *\n * class Movie extends Component {\n *   ﹫provide() static MovieDetails = MovieDetails;\n *\n *   ﹫attribute('string') title!: string;\n *   ﹫attribute('MovieDetails') details!: MovieDetails;\n * }\n * ```\n *\n * > Note that you have to make the `MovieDetails` component accessible from the `Movie` component by using the [`@provide()`](https://layrjs.com/docs/v2/reference/component#provide-decorator) decorator. This way, the `MovieDetails` component can be later referred by its name when you define the `details` attribute using the [`@attribute()`](https://layrjs.com/docs/v2/reference/component#attribute-decorator) decorator.\n *\n * Finally, the `Movie` component can be instantiated like this:\n *\n * ```\n * const movie = new Movie({\n *   title: 'Inception',\n *   details: new Movie.MovieDetails({duration: 120, aspectRatio: '16:9'})\n * });\n *\n * movie.title; // => 'Inception'\n * movie.details.duration; // => 120\n * ```\n */\nexport class EmbeddedComponent extends Component {\n  // === Methods ===\n\n  /**\n   * See the methods that are inherited from the [`Component`](https://layrjs.com/docs/v2/reference/component#creation) class.\n   *\n   * @category Methods\n   */\n\n  // === Observability ===\n\n  /**\n   * See the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) class.\n   *\n   * @category Observability\n   */\n\n  /**\n   * Always returns `true`.\n   *\n   * @category Embeddability\n   */\n  static isEmbedded() {\n    return true;\n  }\n}\n"
  },
  {
    "path": "packages/component/src/forking.test.ts",
    "content": "import {Component} from './component';\nimport {attribute, provide} from './decorators';\n\ndescribe('Forking', () => {\n  test('Simple component', async () => {\n    class Movie extends Component {\n      @attribute() static limit = 100;\n\n      @attribute() title!: string;\n      @attribute() tags!: string[];\n      @attribute() specs!: {duration?: number};\n    }\n\n    const MovieFork = Movie.fork();\n\n    expect(MovieFork.getComponentType()).toBe('typeof Movie');\n    expect(MovieFork.limit).toBe(100);\n\n    expect(MovieFork.isForkOf(Movie)).toBe(true);\n    expect(MovieFork.isForkOf(MovieFork)).toBe(false);\n    expect(Movie.isForkOf(MovieFork)).toBe(false);\n\n    MovieFork.limit = 500;\n\n    expect(MovieFork.limit).toBe(500);\n    expect(Movie.limit).toBe(100);\n\n    const GhostMovie = Movie.getGhost();\n    const SameGhostMovie = Movie.getGhost();\n\n    expect(GhostMovie.isForkOf(Movie)).toBe(true);\n    expect(SameGhostMovie).toBe(GhostMovie);\n\n    const movie = new Movie({title: 'Inception', tags: ['drama'], specs: {duration: 120}});\n\n    expect(movie).toBeInstanceOf(Component);\n    expect(movie).toBeInstanceOf(Movie);\n\n    let movieFork = movie.fork();\n\n    expect(movieFork).toBeInstanceOf(Component);\n    expect(movieFork).toBeInstanceOf(Movie);\n\n    expect(movieFork.getComponentType()).toBe('Movie');\n    expect(movieFork.title).toBe('Inception');\n    expect(movieFork.tags).toEqual(['drama']);\n    expect(movieFork.specs).toEqual({duration: 120});\n\n    expect(movieFork.isForkOf(movie)).toBe(true);\n    expect(movieFork.isForkOf(movieFork)).toBe(false);\n    expect(movie.isForkOf(movieFork)).toBe(false);\n\n    movieFork.title = 'Inception 2';\n    movieFork.tags.push('action');\n    movieFork.specs.duration = 125;\n\n    expect(movieFork.title).toBe('Inception 2');\n    expect(movieFork.tags).toEqual(['drama', 'action']);\n    expect(movieFork.specs).toEqual({duration: 125});\n    expect(movie.title).toBe('Inception');\n    expect(movie.tags).toEqual(['drama']);\n    expect(movie.specs).toEqual({duration: 120});\n\n    movieFork = movie.fork({componentClass: MovieFork});\n\n    expect(movieFork).toBeInstanceOf(Component);\n    expect(movieFork).toBeInstanceOf(Movie);\n    expect(movieFork).toBeInstanceOf(MovieFork);\n\n    expect(() => movie.getGhost()).toThrow(\n      \"Cannot get the identifiers of a component that has no set identifier (component: 'Movie')\"\n    );\n  });\n\n  test('Component provision', async () => {\n    class MovieDetails extends Component {}\n\n    class Movie extends Component {\n      @provide() static MovieDetails = MovieDetails;\n    }\n\n    class App extends Component {\n      @provide() static Movie = Movie;\n    }\n\n    const GhostApp = App.getGhost();\n    const SameGhostApp = App.getGhost();\n\n    expect(GhostApp.isForkOf(App)).toBe(true);\n    expect(SameGhostApp).toBe(GhostApp);\n\n    const GhostMovie = Movie.getGhost();\n    const SameGhostMovie = Movie.getGhost();\n\n    expect(GhostMovie.isForkOf(Movie)).toBe(true);\n    expect(SameGhostMovie).toBe(GhostMovie);\n    expect(GhostApp.Movie).toBe(GhostMovie);\n\n    const GhostMovieDetails = MovieDetails.getGhost();\n    const SameGhostMovieDetails = MovieDetails.getGhost();\n\n    expect(GhostMovieDetails.isForkOf(MovieDetails)).toBe(true);\n    expect(SameGhostMovieDetails).toBe(GhostMovieDetails);\n    expect(GhostMovie.MovieDetails).toBe(GhostMovieDetails);\n    expect(GhostApp.Movie.MovieDetails).toBe(GhostMovieDetails);\n  });\n\n  test('Referenced component', async () => {\n    class Director extends Component {\n      @attribute() name!: string;\n    }\n\n    class Movie extends Component {\n      @provide() static Director = Director;\n\n      @attribute() director!: Director;\n    }\n\n    const movie = new Movie({director: new Director({name: 'Christopher Nolan'})});\n\n    const movieFork = movie.fork();\n\n    expect(movieFork.director).not.toBe(movie.director);\n    expect(movieFork.director.name).toBe('Christopher Nolan');\n    expect(movieFork.director.constructor.isForkOf(Director)).toBe(true);\n    expect(movieFork.director.isForkOf(movie.director)).toBe(true);\n\n    movieFork.director.name = 'Christopher Nolan 2';\n\n    expect(movieFork.director.name).toBe('Christopher Nolan 2');\n    expect(movie.director.name).toBe('Christopher Nolan');\n  });\n});\n"
  },
  {
    "path": "packages/component/src/forking.ts",
    "content": "import {fork as simpleFork, ForkOptions as SimpleForkOptions} from 'simple-forking';\n\nimport type {Component} from './component';\nimport {isComponentClass, isComponentInstance} from './utilities';\n\nexport type ForkOptions = SimpleForkOptions & {\n  componentProvider?: typeof Component;\n  componentClass?: typeof Component;\n};\n\n/**\n * Fork any type of values including objects, arrays, and components (using Component's `fork()` [class method](https://layrjs.com/docs/v2/reference/component#fork-class-method) and [instance method](https://layrjs.com/docs/v2/reference/component#fork-instance-method)).\n *\n * @param value A value of any type.\n *\n * @returns A fork of the specified value.\n *\n * @example\n * ```\n * import {fork} from '﹫layr/component';\n *\n * const data = {\n *   token: 'xyz123',\n *   timestamp: 1596600889609,\n *   movie: new Movie({title: 'Inception'})\n * };\n *\n * const dataFork = fork(data);\n * Object.getPrototypeOf(dataFork); // => data\n * dataFork.token; // => 'xyz123';\n * dataFork.timestamp; // => 1596600889609\n * dataFork.movie.isForkOf(data.movie); // => true\n * ```\n *\n * @category Forking\n */\nexport function fork(value: any, options: ForkOptions = {}) {\n  const {objectForker: originalObjectForker, ...otherOptions} = options;\n\n  const objectForker = function (object: object): object | void {\n    if (originalObjectForker !== undefined) {\n      const objectFork = originalObjectForker(object);\n\n      if (objectFork !== undefined) {\n        return objectFork;\n      }\n    }\n\n    if (isComponentClass(object)) {\n      return object.fork(options);\n    }\n\n    if (isComponentInstance(object)) {\n      return object.fork(options);\n    }\n  };\n\n  return simpleFork(value, {...otherOptions, objectForker});\n}\n"
  },
  {
    "path": "packages/component/src/identifiable-component.test.ts",
    "content": "import {Component} from './component';\nimport {\n  isIdentifierAttributeInstance,\n  isPrimaryIdentifierAttributeInstance,\n  isSecondaryIdentifierAttributeInstance\n} from './properties';\nimport {attribute, primaryIdentifier, secondaryIdentifier, provide} from './decorators';\nimport {deserialize} from './deserialization';\n\ndescribe('Identifiable component', () => {\n  test('new ()', async () => {\n    class User extends Component {\n      @primaryIdentifier() id!: string;\n      @secondaryIdentifier() email!: string;\n      @secondaryIdentifier() username!: string;\n    }\n\n    const user = new User({email: 'hi@hello.com', username: 'hi'});\n\n    expect(user.id.length >= 25).toBe(true);\n    expect(user.email).toBe('hi@hello.com');\n    expect(user.username).toBe('hi');\n\n    expect(() => new User({id: user.id, email: 'user1@email.com', username: 'user1'})).toThrow(\n      \"A component with the same identifier already exists (attribute: 'User.prototype.id')\"\n    );\n\n    expect(() => new User({email: 'hi@hello.com', username: 'user2'})).toThrow(\n      \"A component with the same identifier already exists (attribute: 'User.prototype.email')\"\n    );\n\n    expect(() => new User({email: 'user3@email.com', username: 'hi'})).toThrow(\n      \"A component with the same identifier already exists (attribute: 'User.prototype.username')\"\n    );\n\n    expect(() => new User()).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'User.prototype.email', expected type: 'string', received type: 'undefined')\"\n    );\n  });\n\n  test('instantiate()', async () => {\n    class User extends Component {\n      @primaryIdentifier() id!: string;\n      @secondaryIdentifier() email!: string;\n      @attribute('string') name = '';\n    }\n\n    let user = User.instantiate({id: 'abc123'});\n\n    expect(user.id).toBe('abc123');\n    expect(user.getAttribute('email').isSet()).toBe(false);\n    expect(user.getAttribute('name').isSet()).toBe(false);\n\n    let sameUser = User.instantiate({id: 'abc123'});\n\n    expect(sameUser).toBe(user);\n\n    sameUser = User.instantiate('abc123');\n\n    expect(sameUser).toBe(user);\n\n    user = User.instantiate({email: 'hi@hello.com'});\n\n    expect(user.email).toBe('hi@hello.com');\n    expect(user.getAttribute('id').isSet()).toBe(false);\n    expect(user.getAttribute('name').isSet()).toBe(false);\n\n    sameUser = User.instantiate({email: 'hi@hello.com'});\n\n    expect(sameUser).toBe(user);\n\n    expect(() => User.instantiate()).toThrow(\n      \"An identifier is required to instantiate an identifiable component, but received a value of type 'undefined' (component: 'User')\"\n    );\n\n    expect(() => User.instantiate({})).toThrow(\n      \"An identifier selector should be a string, a number, or a non-empty object, but received an empty object (component: 'User')\"\n    );\n\n    expect(() => User.instantiate({name: 'john'})).toThrow(\n      \"A property with the specified name was found, but it is not an identifier attribute (attribute: 'User.prototype.name')\"\n    );\n  });\n\n  test('getIdentifierAttribute()', async () => {\n    class User extends Component {\n      @primaryIdentifier() id!: string;\n      @secondaryIdentifier() email!: string;\n      @attribute('string') name = '';\n    }\n\n    let identifierAttribute = User.prototype.getIdentifierAttribute('id');\n\n    expect(isIdentifierAttributeInstance(identifierAttribute)).toBe(true);\n    expect(identifierAttribute.getName()).toBe('id');\n    expect(identifierAttribute.getParent()).toBe(User.prototype);\n\n    identifierAttribute = User.prototype.getIdentifierAttribute('email');\n\n    expect(isIdentifierAttributeInstance(identifierAttribute)).toBe(true);\n    expect(identifierAttribute.getName()).toBe('email');\n    expect(identifierAttribute.getParent()).toBe(User.prototype);\n\n    expect(() => User.prototype.getIdentifierAttribute('name')).toThrow(\n      \"A property with the specified name was found, but it is not an identifier attribute (attribute: 'User.prototype.name')\"\n    );\n  });\n\n  test('hasIdentifierAttribute()', async () => {\n    class User extends Component {\n      @primaryIdentifier() id!: string;\n      @secondaryIdentifier() email!: string;\n      @attribute('string') name = '';\n    }\n\n    expect(User.prototype.hasIdentifierAttribute('id')).toBe(true);\n    expect(User.prototype.hasIdentifierAttribute('email')).toBe(true);\n    expect(User.prototype.hasIdentifierAttribute('username')).toBe(false);\n\n    expect(() => User.prototype.hasIdentifierAttribute('name')).toThrow(\n      \"A property with the specified name was found, but it is not an identifier attribute (attribute: 'User.prototype.name')\"\n    );\n  });\n\n  test('getPrimaryIdentifierAttribute()', async () => {\n    class User extends Component {\n      @primaryIdentifier() id!: string;\n    }\n\n    const identifierAttribute = User.prototype.getPrimaryIdentifierAttribute();\n\n    expect(isPrimaryIdentifierAttributeInstance(identifierAttribute)).toBe(true);\n    expect(identifierAttribute.getName()).toBe('id');\n    expect(identifierAttribute.getParent()).toBe(User.prototype);\n\n    class Movie extends Component {\n      @secondaryIdentifier() slug!: string;\n    }\n\n    expect(() => Movie.prototype.getPrimaryIdentifierAttribute()).toThrow(\n      \"The component 'Movie' doesn't have a primary identifier attribute\"\n    );\n  });\n\n  test('hasPrimaryIdentifierAttribute()', async () => {\n    class User extends Component {\n      @primaryIdentifier() id!: string;\n    }\n\n    expect(User.prototype.hasPrimaryIdentifierAttribute()).toBe(true);\n\n    class Movie extends Component {\n      @secondaryIdentifier() slug!: string;\n    }\n\n    expect(Movie.prototype.hasPrimaryIdentifierAttribute()).toBe(false);\n  });\n\n  test('setPrimaryIdentifierAttribute()', async () => {\n    class User extends Component {}\n\n    expect(User.prototype.hasPrimaryIdentifierAttribute()).toBe(false);\n\n    const setPrimaryIdentifierAttributeResult = User.prototype.setPrimaryIdentifierAttribute('id');\n\n    expect(User.prototype.hasPrimaryIdentifierAttribute()).toBe(true);\n\n    const primaryIdentifierAttribute = User.prototype.getPrimaryIdentifierAttribute();\n\n    expect(primaryIdentifierAttribute).toBe(setPrimaryIdentifierAttributeResult);\n    expect(isPrimaryIdentifierAttributeInstance(primaryIdentifierAttribute)).toBe(true);\n    expect(primaryIdentifierAttribute.getName()).toBe('id');\n    expect(primaryIdentifierAttribute.getParent()).toBe(User.prototype);\n\n    expect(() => User.prototype.setPrimaryIdentifierAttribute('email')).toThrow(\n      \"The component 'User' already has a primary identifier attribute\"\n    );\n  });\n\n  test('getSecondaryIdentifierAttribute()', async () => {\n    class User extends Component {\n      @primaryIdentifier() id!: string;\n      @secondaryIdentifier() email!: string;\n    }\n\n    const identifierAttribute = User.prototype.getSecondaryIdentifierAttribute('email');\n\n    expect(isSecondaryIdentifierAttributeInstance(identifierAttribute)).toBe(true);\n    expect(identifierAttribute.getName()).toBe('email');\n    expect(identifierAttribute.getParent()).toBe(User.prototype);\n\n    expect(() => User.prototype.getSecondaryIdentifierAttribute('id')).toThrow(\n      \"A property with the specified name was found, but it is not a secondary identifier attribute (attribute: 'User.prototype.id')\"\n    );\n  });\n\n  test('hasSecondaryIdentifierAttribute()', async () => {\n    class User extends Component {\n      @primaryIdentifier() id!: string;\n      @secondaryIdentifier() email!: string;\n      @attribute('string') name = '';\n    }\n\n    expect(User.prototype.hasSecondaryIdentifierAttribute('email')).toBe(true);\n    expect(User.prototype.hasSecondaryIdentifierAttribute('username')).toBe(false);\n\n    expect(() => User.prototype.hasSecondaryIdentifierAttribute('id')).toThrow(\n      \"A property with the specified name was found, but it is not a secondary identifier attribute (attribute: 'User.prototype.id')\"\n    );\n    expect(() => User.prototype.hasSecondaryIdentifierAttribute('name')).toThrow(\n      \"A property with the specified name was found, but it is not a secondary identifier attribute (attribute: 'User.prototype.name')\"\n    );\n  });\n\n  test('setSecondaryIdentifierAttribute()', async () => {\n    class User extends Component {}\n\n    expect(User.prototype.hasSecondaryIdentifierAttribute('email')).toBe(false);\n\n    const setSecondaryIdentifierAttributeResult = User.prototype.setSecondaryIdentifierAttribute(\n      'email'\n    );\n\n    expect(User.prototype.hasSecondaryIdentifierAttribute('email')).toBe(true);\n\n    const secondaryIdentifierAttribute = User.prototype.getSecondaryIdentifierAttribute('email');\n\n    expect(secondaryIdentifierAttribute).toBe(setSecondaryIdentifierAttributeResult);\n    expect(isSecondaryIdentifierAttributeInstance(secondaryIdentifierAttribute)).toBe(true);\n    expect(secondaryIdentifierAttribute.getName()).toBe('email');\n    expect(secondaryIdentifierAttribute.getParent()).toBe(User.prototype);\n\n    expect(() => User.prototype.setSecondaryIdentifierAttribute('username')).not.toThrow();\n  });\n\n  test('getIdentifierAttributes()', async () => {\n    class User extends Component {\n      @primaryIdentifier() id!: string;\n      @secondaryIdentifier() email!: string;\n      @secondaryIdentifier() username!: string;\n      @attribute('string') name = '';\n    }\n\n    const identifierAttributes = User.prototype.getIdentifierAttributes();\n\n    expect(typeof identifierAttributes[Symbol.iterator]).toBe('function');\n    expect(Array.from(identifierAttributes).map((property) => property.getName())).toEqual([\n      'id',\n      'email',\n      'username'\n    ]);\n  });\n\n  test('getSecondaryIdentifierAttributes()', async () => {\n    class User extends Component {\n      @primaryIdentifier() id!: string;\n      @secondaryIdentifier() email!: string;\n      @secondaryIdentifier() username!: string;\n      @attribute('string') name = '';\n    }\n\n    const secondaryIdentifierAttributes = User.prototype.getSecondaryIdentifierAttributes();\n\n    expect(typeof secondaryIdentifierAttributes[Symbol.iterator]).toBe('function');\n    expect(\n      Array.from(secondaryIdentifierAttributes).map((property) => property.getName())\n    ).toEqual(['email', 'username']);\n  });\n\n  test('getIdentifiers()', async () => {\n    class User extends Component {\n      @primaryIdentifier() id!: string;\n      @secondaryIdentifier() email!: string;\n    }\n\n    let user = User.fork().instantiate({id: 'abc123'});\n\n    expect(user.getIdentifiers()).toStrictEqual({id: 'abc123'});\n\n    user.email = 'hi@hello.com';\n\n    expect(user.getIdentifiers()).toStrictEqual({id: 'abc123', email: 'hi@hello.com'});\n\n    user = User.fork().instantiate({email: 'hi@hello.com'});\n\n    expect(user.getIdentifiers()).toStrictEqual({email: 'hi@hello.com'});\n  });\n\n  test('getIdentifierDescriptor()', async () => {\n    class User extends Component {\n      @primaryIdentifier() id!: string;\n      @secondaryIdentifier() email!: string;\n    }\n\n    let user = User.fork().instantiate({id: 'abc123'});\n\n    expect(user.getIdentifierDescriptor()).toStrictEqual({id: 'abc123'});\n\n    user.email = 'hi@hello.com';\n\n    expect(user.getIdentifierDescriptor()).toStrictEqual({id: 'abc123'});\n\n    user = User.fork().instantiate({email: 'hi@hello.com'});\n\n    expect(user.getIdentifierDescriptor()).toStrictEqual({email: 'hi@hello.com'});\n\n    user.id = 'abc123';\n\n    expect(user.getIdentifierDescriptor()).toStrictEqual({id: 'abc123'});\n  });\n\n  test('normalizeIdentifierDescriptor()', async () => {\n    class User extends Component {\n      @primaryIdentifier() id!: string;\n      @secondaryIdentifier() email!: string;\n      @secondaryIdentifier('number') reference!: number;\n      @attribute('string') name = '';\n    }\n\n    expect(User.normalizeIdentifierDescriptor('abc123')).toStrictEqual({id: 'abc123'});\n    expect(User.normalizeIdentifierDescriptor({id: 'abc123'})).toStrictEqual({id: 'abc123'});\n    expect(User.normalizeIdentifierDescriptor({email: 'hi@hello.com'})).toStrictEqual({\n      email: 'hi@hello.com'\n    });\n    expect(User.normalizeIdentifierDescriptor({reference: 123456})).toStrictEqual({\n      reference: 123456\n    });\n\n    // @ts-expect-error\n    expect(() => User.normalizeIdentifierDescriptor(undefined)).toThrow(\n      \"An identifier descriptor should be a string, a number, or an object, but received a value of type 'undefined' (component: 'User')\"\n    );\n    // @ts-expect-error\n    expect(() => User.normalizeIdentifierDescriptor(true)).toThrow(\n      \"An identifier descriptor should be a string, a number, or an object, but received a value of type 'boolean' (component: 'User')\"\n    );\n    // @ts-expect-error\n    expect(() => User.normalizeIdentifierDescriptor([])).toThrow(\n      \"An identifier descriptor should be a string, a number, or an object, but received a value of type 'Array' (component: 'User')\"\n    );\n    expect(() => User.normalizeIdentifierDescriptor({})).toThrow(\n      \"An identifier descriptor should be a string, a number, or an object composed of one attribute, but received an object composed of 0 attributes (component: 'User', received object: {})\"\n    );\n    expect(() => User.normalizeIdentifierDescriptor({id: 'abc123', email: 'hi@hello.com'})).toThrow(\n      'An identifier descriptor should be a string, a number, or an object composed of one attribute, but received an object composed of 2 attributes (component: \\'User\\', received object: {\"id\":\"abc123\",\"email\":\"hi@hello.com\"})'\n    );\n    expect(() => User.normalizeIdentifierDescriptor(123456)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'User.prototype.id', expected type: 'string', received type: 'number')\"\n    );\n    expect(() => User.normalizeIdentifierDescriptor({email: 123456})).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'User.prototype.email', expected type: 'string', received type: 'number')\"\n    );\n    expect(() => User.normalizeIdentifierDescriptor({reference: 'abc123'})).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'User.prototype.reference', expected type: 'number', received type: 'string')\"\n    );\n    // @ts-expect-error\n    expect(() => User.normalizeIdentifierDescriptor({email: undefined})).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'User.prototype.email', expected type: 'string', received type: 'undefined')\"\n    );\n    expect(() => User.normalizeIdentifierDescriptor({name: 'john'})).toThrow(\n      \"A property with the specified name was found, but it is not an identifier attribute (attribute: 'User.prototype.name')\"\n    );\n    expect(() => User.normalizeIdentifierDescriptor({country: 'USA'})).toThrow(\n      \"The identifier attribute 'country' is missing (component: 'User')\"\n    );\n  });\n\n  test('describeIdentifierDescriptor()', async () => {\n    class User extends Component {\n      @primaryIdentifier() id!: string;\n      @secondaryIdentifier() email!: string;\n      @secondaryIdentifier('number') reference!: number;\n    }\n\n    expect(User.describeIdentifierDescriptor('abc123')).toBe(\"id: 'abc123'\");\n    expect(User.describeIdentifierDescriptor({id: 'abc123'})).toBe(\"id: 'abc123'\");\n    expect(User.describeIdentifierDescriptor({email: 'hi@hello.com'})).toBe(\n      \"email: 'hi@hello.com'\"\n    );\n    expect(User.describeIdentifierDescriptor({reference: 123456})).toBe('reference: 123456');\n  });\n\n  test('resolveAttributeSelector()', async () => {\n    class Person extends Component {\n      @primaryIdentifier() id!: string;\n      @attribute('string') name = '';\n    }\n\n    class Movie extends Component {\n      @provide() static Person = Person;\n\n      @primaryIdentifier() id!: string;\n      @attribute('string') title = '';\n      @attribute('Person?') director?: Person;\n    }\n\n    expect(Movie.prototype.resolveAttributeSelector(true)).toStrictEqual({\n      id: true,\n      title: true,\n      director: {id: true}\n    });\n\n    expect(\n      Movie.prototype.resolveAttributeSelector(true, {includeReferencedComponents: true})\n    ).toStrictEqual({\n      id: true,\n      title: true,\n      director: {id: true, name: true}\n    });\n\n    expect(Movie.prototype.resolveAttributeSelector(false)).toStrictEqual({});\n\n    expect(Movie.prototype.resolveAttributeSelector({})).toStrictEqual({id: true});\n\n    expect(Movie.prototype.resolveAttributeSelector({title: true})).toStrictEqual({\n      id: true,\n      title: true\n    });\n\n    expect(Movie.prototype.resolveAttributeSelector({director: true})).toStrictEqual({\n      id: true,\n      director: {id: true}\n    });\n\n    expect(\n      Movie.prototype.resolveAttributeSelector(\n        {director: true},\n        {includeReferencedComponents: true}\n      )\n    ).toStrictEqual({\n      id: true,\n      director: {id: true, name: true}\n    });\n\n    expect(Movie.prototype.resolveAttributeSelector({director: false})).toStrictEqual({\n      id: true\n    });\n\n    expect(Movie.prototype.resolveAttributeSelector({director: {}})).toStrictEqual({\n      id: true,\n      director: {id: true}\n    });\n\n    expect(\n      Movie.prototype.resolveAttributeSelector({director: {}}, {includeReferencedComponents: true})\n    ).toStrictEqual({\n      id: true,\n      director: {id: true}\n    });\n  });\n\n  test('generateId()', async () => {\n    class Movie extends Component {}\n\n    const id1 = Movie.generateId();\n\n    expect(typeof id1).toBe('string');\n    expect(id1.length >= 25).toBe(true);\n\n    const id2 = Movie.generateId();\n\n    expect(typeof id2).toBe('string');\n    expect(id2.length >= 25).toBe(true);\n    expect(id2).not.toBe(id1);\n  });\n\n  test('fork()', async () => {\n    class User extends Component {\n      @primaryIdentifier() id!: string;\n      @secondaryIdentifier() email!: string;\n    }\n\n    const user = new User({id: 'abc123', email: 'hi@hello.com'});\n\n    expect(user.id).toBe('abc123');\n    expect(user.email).toBe('hi@hello.com');\n\n    let UserFork = User.fork();\n\n    const userFork = UserFork.getIdentityMap().getComponent({id: 'abc123'}) as User;\n\n    expect(userFork.constructor).toBe(UserFork);\n    expect(userFork).toBeInstanceOf(UserFork);\n\n    expect(userFork.isForkOf(user)).toBe(true);\n    expect(userFork).not.toBe(user);\n    expect(userFork.id).toBe('abc123');\n    expect(userFork.email).toBe('hi@hello.com');\n\n    // --- With a referenced identifiable component ---\n\n    class Article extends Component {\n      @provide() static User = User;\n\n      @primaryIdentifier() id!: string;\n      @attribute('string') title = '';\n      @attribute('User') author!: User;\n    }\n\n    const article = new Article({id: 'xyz456', title: 'Hello', author: user});\n\n    expect(article.id).toBe('xyz456');\n    expect(article.title).toBe('Hello');\n\n    const author = article.author;\n\n    expect(author).toBe(user);\n\n    const ArticleFork = Article.fork();\n\n    const articleFork = ArticleFork.getIdentityMap().getComponent({id: 'xyz456'}) as Article;\n\n    expect(articleFork.constructor).toBe(ArticleFork);\n    expect(articleFork).toBeInstanceOf(ArticleFork);\n\n    expect(articleFork.isForkOf(article)).toBe(true);\n    expect(articleFork).not.toBe(article);\n    expect(articleFork.id).toBe('xyz456');\n    expect(articleFork.title).toBe('Hello');\n\n    const authorFork = articleFork.author;\n\n    UserFork = ArticleFork.User;\n\n    expect(authorFork.constructor).toBe(UserFork);\n    expect(authorFork).toBeInstanceOf(UserFork);\n\n    expect(authorFork.isForkOf(author)).toBe(true);\n    expect(authorFork).not.toBe(author);\n    expect(authorFork.id).toBe('abc123');\n    expect(authorFork.email).toBe('hi@hello.com');\n\n    expect(UserFork.getIdentityMap().getComponent({id: 'abc123'})).toBe(authorFork);\n\n    // --- With a serialized referenced identifiable component ---\n\n    const deserializedArticle = deserialize(\n      {\n        __component: 'Article',\n        id: 'xyz789',\n        title: 'Hello 2',\n        author: {__component: 'User', id: 'abc123'}\n      },\n      {rootComponent: ArticleFork}\n    ) as Article;\n\n    const deserializedAuthor = deserializedArticle.author;\n\n    expect(deserializedAuthor.constructor).toBe(UserFork);\n    expect(deserializedAuthor).toBeInstanceOf(UserFork);\n\n    expect(deserializedAuthor.isForkOf(author)).toBe(true);\n    expect(deserializedAuthor).not.toBe(author);\n    expect(deserializedAuthor.id).toBe('abc123');\n    expect(deserializedAuthor.email).toBe('hi@hello.com');\n\n    expect(deserializedAuthor).toBe(authorFork);\n  });\n\n  test('getGhost()', async () => {\n    class User extends Component {\n      @primaryIdentifier() id!: string;\n    }\n\n    class App extends Component {\n      @provide() static User = User;\n    }\n\n    const user = new User();\n    const ghostUser = user.getGhost();\n\n    expect(ghostUser.isForkOf(user)).toBe(true);\n    expect(ghostUser.constructor).toBe(App.getGhost().User);\n\n    const sameGhostUser = user.getGhost();\n\n    expect(sameGhostUser).toBe(ghostUser);\n  });\n\n  test('detach()', async () => {\n    class User extends Component {\n      @primaryIdentifier() id!: string;\n    }\n\n    const user = User.instantiate({id: 'abc123'});\n    const sameUser = User.getIdentityMap().getComponent({id: 'abc123'});\n\n    expect(sameUser).toBe(user);\n\n    user.detach();\n\n    const otherUser = User.instantiate({id: 'abc123'});\n    const sameOtherUser = User.getIdentityMap().getComponent({id: 'abc123'});\n\n    expect(otherUser).not.toBe(user);\n    expect(sameOtherUser).toBe(otherUser);\n\n    User.detach();\n\n    const user2 = User.instantiate({id: 'xyz456'});\n    const otherUser2 = User.instantiate({id: 'xyz456'});\n\n    expect(otherUser2).not.toBe(user2);\n  });\n\n  test('toObject()', async () => {\n    class Director extends Component {\n      @primaryIdentifier() id!: string;\n      @attribute() name = '';\n    }\n\n    class Movie extends Component {\n      @provide() static Director = Director;\n\n      @primaryIdentifier() id!: string;\n      @attribute() title = '';\n      @attribute('Director') director!: Director;\n    }\n\n    let movie = new Movie({\n      id: 'm1',\n      title: 'Inception',\n      director: new Director({id: 'd1', name: 'Christopher Nolan'})\n    });\n\n    expect(movie.toObject()).toStrictEqual({id: 'm1', title: 'Inception', director: {id: 'd1'}});\n    expect(movie.toObject({minimize: true})).toStrictEqual({id: 'm1'});\n  });\n});\n"
  },
  {
    "path": "packages/component/src/identity-map.test.ts",
    "content": "import {Component} from './component';\nimport {IdentityMap} from './identity-map';\nimport {primaryIdentifier, secondaryIdentifier} from './decorators';\n\ndescribe('Identity map', () => {\n  test('new IdentityMap()', async () => {\n    class User extends Component {\n      @primaryIdentifier() id!: string;\n    }\n\n    const identityMap = new IdentityMap(User);\n\n    expect(identityMap.getParent()).toBe(User);\n  });\n\n  test('getComponent()', async () => {\n    class User extends Component {\n      @primaryIdentifier() id!: string;\n    }\n\n    const identityMap = new IdentityMap(User);\n\n    expect(identityMap.getComponent({id: 'abc123'})).toBeUndefined();\n\n    const user = new User({id: 'abc123'});\n\n    identityMap.addComponent(user);\n\n    expect(identityMap.getComponent({id: 'abc123'})).toBe(user);\n    expect(identityMap.getComponent('abc123')).toBe(user);\n    expect(identityMap.getComponent({id: 'xyz456'})).toBeUndefined();\n  });\n\n  test('addComponent()', async () => {\n    class User extends Component {\n      @primaryIdentifier() id!: string;\n    }\n\n    const identityMap = new IdentityMap(User);\n    const user = new User({id: 'abc123'});\n\n    identityMap.addComponent(user);\n\n    expect(identityMap.getComponent({id: 'abc123'})).toBe(user);\n\n    expect(() => identityMap.addComponent(user)).toThrow(\n      \"A component with the same identifier already exists (attribute: 'User.prototype.id')\"\n    );\n\n    user.detach();\n\n    expect(() => identityMap.addComponent(user)).toThrow(\n      \"Cannot add a detached component to the identity map (component: 'User')\"\n    );\n  });\n\n  test('updateComponent()', async () => {\n    class User extends Component {\n      @primaryIdentifier() id!: string;\n      @secondaryIdentifier() email!: string;\n    }\n\n    const identityMap = new IdentityMap(User);\n\n    const user = new User({id: 'abc123', email: 'hi@hello.com'});\n\n    identityMap.addComponent(user);\n\n    expect(identityMap.getComponent({email: 'hi@hello.com'})).toBe(user);\n\n    user.email = 'salut@bonjour.com';\n\n    identityMap.updateComponent(user, 'email', {\n      previousValue: 'hi@hello.com',\n      newValue: 'salut@bonjour.com'\n    });\n\n    expect(identityMap.getComponent({email: 'salut@bonjour.com'})).toBe(user);\n    expect(identityMap.getComponent({email: 'hi@hello.com'})).toBe(undefined);\n\n    const otherUser = new User({id: 'xyz456', email: 'hi@hello.com'});\n\n    identityMap.addComponent(otherUser);\n\n    expect(identityMap.getComponent({email: 'hi@hello.com'})).toBe(otherUser);\n\n    expect(() =>\n      identityMap.updateComponent(otherUser, 'email', {\n        previousValue: 'hi@hello.com',\n        newValue: 'salut@bonjour.com'\n      })\n    ).toThrow(\n      \"A component with the same identifier already exists (attribute: 'User.prototype.email')\"\n    );\n\n    // --- Forking ---\n\n    const UserFork = User.fork();\n\n    const identityMapFork = identityMap.fork(UserFork);\n\n    const userFork = identityMapFork.getComponent({email: 'salut@bonjour.com'}) as User;\n\n    expect(userFork.isForkOf(user)).toBe(true);\n\n    userFork.email = 'hi@hello.com';\n\n    identityMapFork.updateComponent(userFork, 'email', {\n      previousValue: 'salut@bonjour.com',\n      newValue: 'hi@hello.com'\n    });\n\n    expect(identityMapFork.getComponent({email: 'hi@hello.com'})).toBe(userFork);\n\n    expect(() => identityMapFork.getComponent({email: 'salut@bonjour.com'})).toThrow(\n      \"A component with the same identifier already exists (attribute: 'User.prototype.id')\"\n    );\n  });\n\n  test('removeComponent()', async () => {\n    class User extends Component {\n      @primaryIdentifier() id!: string;\n    }\n\n    const identityMap = new IdentityMap(User);\n    const user = new User({id: 'abc123'});\n\n    identityMap.addComponent(user);\n\n    expect(identityMap.getComponent({id: 'abc123'})).toBe(user);\n\n    identityMap.removeComponent(user);\n\n    expect(identityMap.getComponent({id: 'abc123'})).toBeUndefined();\n\n    identityMap.addComponent(user);\n\n    expect(identityMap.getComponent({id: 'abc123'})).toBe(user);\n\n    user.detach();\n\n    expect(() => identityMap.removeComponent(user)).toThrow(\n      \"Cannot remove a detached component from the identity map (component: 'User')\"\n    );\n  });\n\n  test('getComponents()', async () => {\n    class User extends Component {\n      @primaryIdentifier() id!: string;\n    }\n\n    const identityMap = new IdentityMap(User);\n\n    const user1 = new User({id: 'abc123'});\n    identityMap.addComponent(user1);\n\n    expect(Array.from(identityMap.getComponents())).toEqual([user1]);\n\n    const user2 = new User({id: 'xyz456'});\n    identityMap.addComponent(user2);\n\n    expect(Array.from(identityMap.getComponents())).toEqual([user1, user2]);\n  });\n});\n"
  },
  {
    "path": "packages/component/src/identity-map.ts",
    "content": "import {hasOwnProperty} from 'core-helpers';\n\nimport type {Component, IdentifierSelector} from './component';\nimport type {IdentifierValue} from './properties';\n\n/**\n * A class to manage the instances of the [`Component`](https://layrjs.com/docs/v2/reference/component) classes that are identifiable.\n *\n * A component class is identifiable when its prototype has a [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute).\n *\n * When a component class is identifiable, the `IdentityMap` ensures that there can only be one component instance with a specific identifier. So if you try to create two components with the same identifier, you will get an error.\n *\n * #### Usage\n *\n * You shouldn't have to create an `IdentityMap` by yourself. Identity maps are created automatically for each [`Component`](https://layrjs.com/docs/v2/reference/component) class that are identifiable.\n *\n * **Example:**\n *\n * Here is a `Movie` component with an `id` primary identifier attribute:\n *\n * ```\n * // JS\n *\n * import {Component, primaryIdentifier, attribute} from '﹫layr/component';\n *\n * class Movie extends Component {\n *   ﹫primaryIdentifier() id;\n *   ﹫attribute('string') title;\n * }\n * ```\n *\n * ```\n * // TS\n *\n * import {Component, primaryIdentifier, attribute} from '﹫layr/component';\n *\n * class Movie extends Component {\n *   ﹫primaryIdentifier() id!: string;\n *   ﹫attribute('string') title!: string;\n * }\n * ```\n *\n * To get the `IdentityMap` of the `Movie` component, simply do:\n *\n * ```\n * const identityMap = Movie.getIdentityMap();\n * ```\n *\n * Currently, the `IdentifyMap` provides only one public method — [`getComponent()`](https://layrjs.com/docs/v2/reference/identity-map#get-component-instance-method) — that allows to retrieve a component instance from its identifier:\n *\n * ```\n * const movie = new Movie({id: 'abc123', title: 'Inception'});\n *\n * identityMap.getComponent('abc123'); // => movie\n * ```\n */\nexport class IdentityMap {\n  _parent: typeof Component;\n\n  constructor(parent: typeof Component) {\n    this._parent = parent;\n  }\n\n  getParent() {\n    return this._parent;\n  }\n\n  fork(newParent: typeof Component) {\n    const identityMapFork = Object.create(this) as IdentityMap;\n    identityMapFork._parent = newParent;\n    return identityMapFork;\n  }\n\n  // === Entities ===\n\n  /**\n   * Gets a component instance from one of its identifiers. If there are no components corresponding to the specified identifiers, returns `undefined`.\n   *\n   * @param identifiers A plain object specifying some identifiers. The shape of the object should be `{[identifierName]: identifierValue}`. Alternatively, you can specify a string or a number representing the value of the [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute) of the component you want to get.\n   *\n   * @returns A [`Component`](https://layrjs.com/docs/v2/reference/component) instance or `undefined`.\n   *\n   * @example\n   * ```\n   * // JS\n   *\n   * import {Component, primaryIdentifier, secondaryIdentifier} from '﹫layr/component';\n   *\n   * class Movie extends Component {\n   *   ﹫primaryIdentifier() id;\n   *   ﹫secondaryIdentifier() slug;\n   * }\n   *\n   * const movie = new Movie({id: 'abc123', slug: 'inception'});\n   *\n   * Movie.getIdentityMap().getComponent('abc123'); // => movie\n   * Movie.getIdentityMap().getComponent({id: 'abc123'}); // => movie\n   * Movie.getIdentityMap().getComponent({slug: 'inception'}); // => movie\n   * Movie.getIdentityMap().getComponent('xyx456'); // => undefined\n   * ```\n   *\n   * @example\n   * ```\n   * // TS\n   *\n   * import {Component, primaryIdentifier, secondaryIdentifier} from '﹫layr/component';\n   *\n   * class Movie extends Component {\n   *   ﹫primaryIdentifier() id!: string;\n   *   ﹫secondaryIdentifier() slug!: string;\n   * }\n   *\n   * const movie = new Movie({id: 'abc123', slug: 'inception'});\n   *\n   * Movie.getIdentityMap().getComponent('abc123'); // => movie\n   * Movie.getIdentityMap().getComponent({id: 'abc123'}); // => movie\n   * Movie.getIdentityMap().getComponent({slug: 'inception'}); // => movie\n   * Movie.getIdentityMap().getComponent('xyx456'); // => undefined\n   * ```\n   *\n   * @category Methods\n   */\n  getComponent(identifiers: IdentifierSelector = {}) {\n    const parent = this.getParent();\n\n    const normalizedIdentifiers = parent.normalizeIdentifierSelector(identifiers);\n\n    if (parent.isDetached()) {\n      return undefined;\n    }\n\n    for (const identifierAttribute of parent.prototype.getIdentifierAttributes()) {\n      const name = identifierAttribute.getName();\n      const value: IdentifierValue | undefined = normalizedIdentifiers[name];\n\n      if (value === undefined) {\n        continue;\n      }\n\n      const index = this._getIndex(name);\n\n      let component = index[value];\n\n      if (component === undefined) {\n        continue;\n      }\n\n      if (!hasOwnProperty(index, value)) {\n        // The component's class has been forked\n        component = component.fork({componentClass: parent});\n      }\n\n      return component;\n    }\n\n    return undefined;\n  }\n\n  addComponent(component: Component) {\n    if (component.isDetached()) {\n      throw new Error(\n        `Cannot add a detached component to the identity map (${component.describeComponent()})`\n      );\n    }\n\n    for (const identifierAttribute of component.getIdentifierAttributes({\n      setAttributesOnly: true\n    })) {\n      const name = identifierAttribute.getName();\n      const value = identifierAttribute.getValue() as IdentifierValue;\n      const index = this._getIndex(name);\n\n      if (hasOwnProperty(index, value)) {\n        throw new Error(\n          `A component with the same identifier already exists (${index[value]\n            .getAttribute(name)\n            .describe()})`\n        );\n      }\n\n      index[value] = component;\n    }\n  }\n\n  updateComponent(\n    component: Component,\n    attributeName: string,\n    {\n      previousValue,\n      newValue\n    }: {previousValue: IdentifierValue | undefined; newValue: IdentifierValue | undefined}\n  ) {\n    if (component.isDetached()) {\n      return;\n    }\n\n    if (newValue === previousValue) {\n      return;\n    }\n\n    const index = this._getIndex(attributeName);\n\n    if (previousValue !== undefined) {\n      delete index[previousValue];\n    }\n\n    if (newValue !== undefined) {\n      if (hasOwnProperty(index, newValue)) {\n        throw new Error(\n          `A component with the same identifier already exists (${component\n            .getAttribute(attributeName)\n            .describe()})`\n        );\n      }\n\n      index[newValue] = component;\n    }\n  }\n\n  removeComponent(component: Component) {\n    if (component.isDetached()) {\n      throw new Error(\n        `Cannot remove a detached component from the identity map (${component.describeComponent()})`\n      );\n    }\n\n    for (const identifierAttribute of component.getIdentifierAttributes({\n      setAttributesOnly: true\n    })) {\n      const name = identifierAttribute.getName();\n      const value = identifierAttribute.getValue() as IdentifierValue;\n      const index = this._getIndex(name);\n      delete index[value];\n    }\n  }\n\n  getComponents() {\n    const identityMap = this;\n\n    return {\n      *[Symbol.iterator]() {\n        const yieldedComponents = new Set<Component>();\n\n        const indexes = identityMap._getIndexes();\n\n        for (const name in indexes) {\n          const index = identityMap._getIndex(name);\n\n          for (const value in index) {\n            const component = identityMap.getComponent({[name]: value});\n\n            if (component === undefined || yieldedComponents.has(component)) {\n              continue;\n            }\n\n            yield component;\n\n            yieldedComponents.add(component);\n          }\n        }\n      }\n    };\n  }\n\n  // === Indexes ===\n\n  _getIndex(name: string) {\n    const indexes = this._getIndexes();\n\n    if (!indexes[name]) {\n      indexes[name] = Object.create(null);\n    } else if (!hasOwnProperty(indexes, name)) {\n      indexes[name] = Object.create(indexes[name]);\n    }\n\n    return indexes[name];\n  }\n\n  _indexes!: {[name: string]: {[value: string]: Component}};\n\n  _getIndexes() {\n    if (!this._indexes) {\n      this._indexes = Object.create(null);\n    } else if (!hasOwnProperty(this, '_indexes')) {\n      this._indexes = Object.create(this._indexes);\n    }\n\n    return this._indexes;\n  }\n}\n"
  },
  {
    "path": "packages/component/src/index.ts",
    "content": "export * from './cloning';\nexport * from './component';\nexport * from './decorators';\nexport * from './deserialization';\nexport * from './embedded-component';\nexport * from './forking';\nexport * from './identity-map';\nexport * from './merging';\nexport * from './properties';\nexport * from './sanitization';\nexport * from './serialization';\nexport * from './utilities';\nexport * from './validation';\n"
  },
  {
    "path": "packages/component/src/js-parser.ts",
    "content": "import escapeRegExp from 'lodash/escapeRegExp';\n\nexport function getConstructorSourceCode(classSourceCode: string) {\n  let index = classSourceCode.indexOf('constructor(');\n\n  if (index === -1) {\n    return undefined;\n  }\n\n  index = getIndexAfterTerminator(classSourceCode, [')'], index + 'constructor('.length);\n\n  index = classSourceCode.indexOf('{', index);\n\n  if (index === -1) {\n    throw new Error(`Failed to get a constructor's implementation in the specified source code`);\n  }\n\n  const endIndex = getIndexAfterTerminator(classSourceCode, ['}'], index + 1);\n\n  return classSourceCode.slice(index, endIndex);\n}\n\nexport function getAttributeInitializerFromConstructorSourceCode(\n  constructorSourceCode: string,\n  attributeName: string\n) {\n  const regexp = new RegExp(`this\\\\.${escapeRegExp(attributeName)}\\\\s*=\\\\s*`);\n  const attributeMatch = constructorSourceCode.match(regexp);\n\n  if (attributeMatch === null) {\n    return undefined;\n  }\n\n  const index = attributeMatch.index! + attributeMatch[0].length;\n  const endIndex = getIndexAfterTerminator(constructorSourceCode, [';', ',', '}'], index);\n\n  const initializerSourceCode = 'return ' + constructorSourceCode.slice(index, endIndex - 1);\n\n  let initializer: Function;\n\n  try {\n    initializer = new Function(initializerSourceCode);\n  } catch (error) {\n    console.error(\n      `An error occurred while getting the attribute initializer from a constructor source code (failed to create a function from \\`${initializerSourceCode}\\`)`\n    );\n    throw error;\n  }\n\n  Object.defineProperty(initializer, 'name', {\n    value: `${attributeName}Initializer`,\n    configurable: true\n  });\n\n  return initializer;\n}\n\nfunction getIndexAfterTerminator(\n  sourceCode: string,\n  terminators: string[],\n  initialIndex = 0\n): number {\n  let index = initialIndex;\n\n  while (index < sourceCode.length) {\n    for (const terminator of terminators) {\n      if (sourceCode.startsWith(terminator, index)) {\n        return index + terminator.length;\n      }\n    }\n\n    if (sourceCode.startsWith('/*', index)) {\n      index = sourceCode.indexOf('*/', index + 2);\n\n      if (index === -1) {\n        throw new Error(`Couldn't find the comment terminator '*/' in the specified source code`);\n      }\n\n      index = index + 2;\n      continue;\n    }\n\n    if (sourceCode.startsWith('//', index)) {\n      index = sourceCode.indexOf('\\n', index + 2);\n\n      if (index === -1) {\n        index = sourceCode.length;\n      } else {\n        index++;\n      }\n\n      continue;\n    }\n\n    const character = sourceCode[index];\n\n    if (character === \"'\") {\n      index = getIndexAfterStringTerminator(sourceCode, \"'\", index + 1);\n      continue;\n    }\n\n    if (character === '\"') {\n      index = getIndexAfterStringTerminator(sourceCode, '\"', index + 1);\n      continue;\n    }\n\n    if (character === '`') {\n      index = getIndexAfterStringTerminator(sourceCode, '`', index + 1);\n      continue;\n    }\n\n    if (character === '(') {\n      index = getIndexAfterTerminator(sourceCode, [')'], index + 1);\n      continue;\n    }\n\n    if (character === '{') {\n      index = getIndexAfterTerminator(sourceCode, ['}'], index + 1);\n      continue;\n    }\n\n    if (character === '[') {\n      index = getIndexAfterTerminator(sourceCode, [']'], index + 1);\n      continue;\n    }\n\n    index++;\n  }\n\n  throw new Error(\n    `Couldn't find a terminator in the specified source code (terminators: ${JSON.stringify(\n      terminators\n    )})`\n  );\n}\n\nfunction getIndexAfterStringTerminator(\n  sourceCode: string,\n  terminator: string,\n  initialIndex = 0\n): number {\n  let index = initialIndex;\n\n  while (index < sourceCode.length) {\n    const character = sourceCode[index];\n\n    if (character === '\\\\') {\n      index++;\n      continue;\n    }\n\n    if (character === terminator) {\n      return index + 1;\n    }\n\n    if (sourceCode.startsWith('${', index)) {\n      index = getIndexAfterTerminator(sourceCode, ['}'], index + 2);\n      continue;\n    }\n\n    index++;\n  }\n\n  throw new Error(\n    `Couldn't find the string terminator ${JSON.stringify(terminator)} in the specified source code`\n  );\n}\n"
  },
  {
    "path": "packages/component/src/js-tests/decorators.test.js",
    "content": "import {Component} from '../component';\nimport {\n  isAttributeInstance,\n  isPrimaryIdentifierAttributeInstance,\n  isSecondaryIdentifierAttributeInstance,\n  isStringValueTypeInstance,\n  isNumberValueTypeInstance,\n  isMethodInstance\n} from '../properties';\nimport {\n  attribute,\n  primaryIdentifier,\n  secondaryIdentifier,\n  method,\n  expose,\n  provide,\n  consume\n} from '../decorators';\n\ndescribe('Decorators', () => {\n  test('@attribute()', async () => {\n    class Movie extends Component {\n      @attribute() static limit = 100;\n      @attribute() static token;\n\n      @attribute() title = '';\n      @attribute() country;\n    }\n\n    let attr = Movie.getAttribute('limit');\n\n    expect(isAttributeInstance(attr)).toBe(true);\n    expect(attr.getName()).toBe('limit');\n    expect(attr.getParent()).toBe(Movie);\n    expect(attr.getValue()).toBe(100);\n    expect(Movie.limit).toBe(100);\n\n    Movie.limit = 500;\n\n    expect(attr.getValue()).toBe(500);\n    expect(Movie.limit).toBe(500);\n\n    let descriptor = Object.getOwnPropertyDescriptor(Movie, 'limit');\n\n    expect(typeof descriptor.get).toBe('function');\n    expect(typeof descriptor.set).toBe('function');\n\n    attr = Movie.getAttribute('token');\n\n    expect(isAttributeInstance(attr)).toBe(true);\n    expect(attr.getName()).toBe('token');\n    expect(attr.getParent()).toBe(Movie);\n    expect(attr.getValue()).toBeUndefined();\n    expect(Movie.token).toBeUndefined();\n\n    let movie = new Movie();\n\n    attr = movie.getAttribute('title');\n\n    expect(isAttributeInstance(attr)).toBe(true);\n    expect(attr.getName()).toBe('title');\n    expect(attr.getParent()).toBe(movie);\n    expect(typeof attr.getDefault()).toBe('function');\n    expect(attr.evaluateDefault()).toBe('');\n    expect(attr.getValue()).toBe('');\n    expect(movie.title).toBe('');\n\n    movie.title = 'The Matrix';\n\n    expect(attr.getValue()).toBe('The Matrix');\n    expect(movie.title).toBe('The Matrix');\n\n    descriptor = Object.getOwnPropertyDescriptor(Movie.prototype, 'title');\n\n    expect(typeof descriptor.get).toBe('function');\n    expect(typeof descriptor.set).toBe('function');\n\n    expect(Object.getOwnPropertyDescriptor(movie, 'title')).toBe(undefined);\n\n    attr = movie.getAttribute('country');\n\n    expect(isAttributeInstance(attr)).toBe(true);\n    expect(attr.getName()).toBe('country');\n    expect(attr.getParent()).toBe(movie);\n    expect(attr.getDefault()).toBeUndefined();\n    expect(attr.evaluateDefault()).toBeUndefined();\n    expect(attr.getValue()).toBeUndefined();\n    expect(movie.country).toBeUndefined();\n\n    movie.country = 'USA';\n\n    expect(attr.getValue()).toBe('USA');\n    expect(movie.country).toBe('USA');\n\n    expect(Movie.hasAttribute('offset')).toBe(false);\n    expect(() => Movie.getAttribute('offset')).toThrow(\n      \"The attribute 'offset' is missing (component: 'Movie')\"\n    );\n\n    movie = new Movie({title: 'Inception', country: 'USA'});\n\n    expect(movie.title).toBe('Inception');\n    expect(movie.country).toBe('USA');\n\n    class Film extends Movie {\n      @attribute() static limit = 100; // With JS, we cannot inherit the value of static attributes\n      @attribute() static token = '';\n\n      @attribute() title;\n      @attribute() country = '';\n    }\n\n    attr = Film.getAttribute('limit');\n\n    expect(isAttributeInstance(attr)).toBe(true);\n    expect(attr.getName()).toBe('limit');\n    expect(attr.getParent()).toBe(Film);\n    expect(attr.getValue()).toBe(100);\n    expect(Film.limit).toBe(100);\n\n    Film.limit = 1000;\n\n    expect(attr.getValue()).toBe(1000);\n    expect(Film.limit).toBe(1000);\n    expect(Movie.limit).toBe(500);\n\n    attr = Film.getAttribute('token');\n\n    expect(isAttributeInstance(attr)).toBe(true);\n    expect(attr.getName()).toBe('token');\n    expect(attr.getParent()).toBe(Film);\n    expect(attr.getValue()).toBe('');\n    expect(Film.token).toBe('');\n\n    const film = new Film();\n\n    attr = film.getAttribute('title');\n\n    expect(isAttributeInstance(attr)).toBe(true);\n    expect(attr.getName()).toBe('title');\n    expect(attr.getParent()).toBe(film);\n    expect(typeof attr.getDefault()).toBe('function');\n    expect(attr.evaluateDefault()).toBe('');\n    expect(attr.getValue()).toBe('');\n    expect(film.title).toBe('');\n\n    film.title = 'Léon';\n\n    expect(attr.getValue()).toBe('Léon');\n    expect(film.title).toBe('Léon');\n\n    attr = film.getAttribute('country');\n\n    expect(isAttributeInstance(attr)).toBe(true);\n    expect(attr.getName()).toBe('country');\n    expect(attr.getParent()).toBe(film);\n    expect(typeof attr.getDefault()).toBe('function');\n    expect(attr.evaluateDefault()).toBe('');\n    expect(attr.getValue()).toBe('');\n    expect(film.country).toBe('');\n\n    // --- Using getters ---\n\n    class MotionPicture extends Component {\n      @attribute({getter: () => 100}) static limit;\n\n      @attribute({getter: () => 'Untitled'}) title;\n    }\n\n    expect(MotionPicture.limit).toBe(100);\n    expect(MotionPicture.prototype.title).toBe('Untitled');\n\n    expect(() => {\n      class MotionPicture extends Component {\n        @attribute({getter: () => 100}) static limit = 30;\n      }\n\n      return MotionPicture;\n    }).toThrow(\n      \"An attribute cannot have both a getter or setter and an initial value (attribute: 'MotionPicture.limit')\"\n    );\n\n    expect(() => {\n      class MotionPicture extends Component {\n        @attribute({getter: () => 'Untitled'}) title = '';\n      }\n\n      return MotionPicture;\n    }).toThrow(\n      \"An attribute cannot have both a getter or setter and a default value (attribute: 'MotionPicture.prototype.title')\"\n    );\n  });\n\n  test('@primaryIdentifier()', async () => {\n    class Movie1 extends Component {\n      @primaryIdentifier() id;\n    }\n\n    let idAttribute = Movie1.prototype.getPrimaryIdentifierAttribute();\n\n    expect(isPrimaryIdentifierAttributeInstance(idAttribute)).toBe(true);\n    expect(idAttribute.getName()).toBe('id');\n    expect(idAttribute.getParent()).toBe(Movie1.prototype);\n    expect(isStringValueTypeInstance(idAttribute.getValueType())).toBe(true);\n    expect(typeof idAttribute.getDefault()).toBe('function');\n\n    class Movie2 extends Component {\n      @primaryIdentifier('number') id;\n    }\n\n    idAttribute = Movie2.prototype.getPrimaryIdentifierAttribute();\n\n    expect(isPrimaryIdentifierAttributeInstance(idAttribute)).toBe(true);\n    expect(idAttribute.getName()).toBe('id');\n    expect(idAttribute.getParent()).toBe(Movie2.prototype);\n    expect(isNumberValueTypeInstance(idAttribute.getValueType())).toBe(true);\n    expect(idAttribute.getDefault()).toBeUndefined();\n\n    class Movie3 extends Component {\n      @primaryIdentifier('number') id = Math.random();\n    }\n\n    idAttribute = Movie3.prototype.getPrimaryIdentifierAttribute();\n\n    expect(isPrimaryIdentifierAttributeInstance(idAttribute)).toBe(true);\n    expect(idAttribute.getName()).toBe('id');\n    expect(idAttribute.getParent()).toBe(Movie3.prototype);\n    expect(isNumberValueTypeInstance(idAttribute.getValueType())).toBe(true);\n    expect(typeof idAttribute.getDefault()).toBe('function');\n\n    const movie = new Movie3();\n\n    expect(typeof movie.id === 'number').toBe(true);\n\n    expect(() => {\n      class Movie extends Component {\n        @primaryIdentifier() static id;\n      }\n\n      return Movie;\n    }).toThrow(\n      \"Couldn't find a property class while executing @primaryIdentifier() (component: 'Movie', property: 'id')\"\n    );\n\n    expect(() => {\n      class Movie {\n        @primaryIdentifier() id;\n      }\n\n      return Movie;\n    }).toThrow(\"@primaryIdentifier() must be used inside a component class (property: 'id')\");\n\n    expect(() => {\n      class Movie extends Component {\n        @primaryIdentifier() id;\n        @primaryIdentifier() slug;\n      }\n\n      return Movie;\n    }).toThrow(\"The component 'Movie' already has a primary identifier attribute\");\n  });\n\n  test('@secondaryIdentifier()', async () => {\n    class User extends Component {\n      @secondaryIdentifier() email;\n      @secondaryIdentifier() username;\n    }\n\n    const emailAttribute = User.prototype.getSecondaryIdentifierAttribute('email');\n\n    expect(isSecondaryIdentifierAttributeInstance(emailAttribute)).toBe(true);\n    expect(emailAttribute.getName()).toBe('email');\n    expect(emailAttribute.getParent()).toBe(User.prototype);\n    expect(isStringValueTypeInstance(emailAttribute.getValueType())).toBe(true);\n    expect(emailAttribute.getDefault()).toBeUndefined();\n\n    const usernameAttribute = User.prototype.getSecondaryIdentifierAttribute('username');\n\n    expect(isSecondaryIdentifierAttributeInstance(usernameAttribute)).toBe(true);\n    expect(usernameAttribute.getName()).toBe('username');\n    expect(usernameAttribute.getParent()).toBe(User.prototype);\n    expect(isStringValueTypeInstance(usernameAttribute.getValueType())).toBe(true);\n    expect(usernameAttribute.getDefault()).toBeUndefined();\n  });\n\n  test('@method()', async () => {\n    class Movie extends Component {\n      @method() static find() {}\n\n      @method() load() {}\n    }\n\n    expect(typeof Movie.find).toBe('function');\n\n    const movie = new Movie();\n\n    expect(typeof movie.load).toBe('function');\n\n    let meth = Movie.getMethod('find');\n\n    expect(isMethodInstance(meth)).toBe(true);\n    expect(meth.getName()).toBe('find');\n    expect(meth.getParent()).toBe(Movie);\n\n    meth = movie.getMethod('load');\n\n    expect(isMethodInstance(meth)).toBe(true);\n    expect(meth.getName()).toBe('load');\n    expect(meth.getParent()).toBe(movie);\n\n    expect(Movie.hasMethod('delete')).toBe(false);\n    expect(() => Movie.getMethod('delete')).toThrow(\n      \"The method 'delete' is missing (component: 'Movie')\"\n    );\n  });\n\n  test('@expose()', async () => {\n    const testExposure = (componentProvider) => {\n      const component = componentProvider();\n\n      let prop = component.getProperty('limit');\n\n      expect(isAttributeInstance(prop)).toBe(true);\n      expect(prop.getName()).toBe('limit');\n      expect(prop.getExposure()).toStrictEqual({get: true});\n\n      prop = component.getProperty('find');\n\n      expect(isMethodInstance(prop)).toBe(true);\n      expect(prop.getName()).toBe('find');\n      expect(prop.getExposure()).toStrictEqual({call: true});\n\n      prop = component.prototype.getProperty('title');\n\n      expect(isAttributeInstance(prop)).toBe(true);\n      expect(prop.getName()).toBe('title');\n      expect(prop.getExposure()).toStrictEqual({get: true, set: true});\n\n      prop = component.prototype.getProperty('load');\n\n      expect(isMethodInstance(prop)).toBe(true);\n      expect(prop.getName()).toBe('load');\n      expect(prop.getExposure()).toStrictEqual({call: true});\n    };\n\n    testExposure(() => {\n      class Movie extends Component {\n        @expose({get: true}) @attribute() static limit;\n        @expose({call: true}) @method() static find() {}\n\n        @expose({get: true, set: true}) @attribute() title;\n        @expose({call: true}) @method() load() {}\n      }\n\n      return Movie;\n    });\n\n    testExposure(() => {\n      @expose({\n        limit: {get: true},\n        find: {call: true},\n        prototype: {\n          title: {get: true, set: true},\n          load: {call: true}\n        }\n      })\n      class Movie extends Component {\n        @attribute() static limit;\n        @method() static find() {}\n\n        @attribute() title;\n        @method() load() {}\n      }\n\n      return Movie;\n    });\n\n    testExposure(() => {\n      class Movie extends Component {\n        @attribute() static limit;\n        @method() static find() {}\n\n        @attribute() title;\n        @method() load() {}\n      }\n\n      @expose({\n        limit: {get: true},\n        find: {call: true},\n        prototype: {\n          title: {get: true, set: true},\n          load: {call: true}\n        }\n      })\n      class ExposedMovie extends Movie {}\n\n      return ExposedMovie;\n    });\n  });\n\n  test('@provide()', async () => {\n    class Movie extends Component {}\n\n    class Backend extends Component {\n      @provide() static Movie = Movie;\n    }\n\n    expect(Backend.getProvidedComponent('Movie')).toBe(Movie);\n\n    ((Backend, BackendMovie) => {\n      class Movie extends BackendMovie {}\n\n      class Frontend extends Backend {\n        @provide() static Movie = Movie;\n      }\n\n      expect(Frontend.getProvidedComponent('Movie')).toBe(Movie);\n    })(Backend, Movie);\n\n    // The backend should not be affected by the frontend\n    expect(Backend.getProvidedComponent('Movie')).toBe(Movie);\n\n    expect(() => {\n      class Movie extends Component {}\n\n      class Backend extends Component {\n        // @ts-expect-error\n        @provide() Movie = Movie;\n      }\n\n      return Backend;\n    }).toThrow(\n      \"@provide() must be used inside a component class with as static attribute declaration (attribute: 'Movie')\"\n    );\n\n    expect(() => {\n      class Movie {}\n\n      class Backend extends Component {\n        @provide() static Movie = Movie;\n      }\n\n      return Backend;\n    }).toThrow(\n      \"@provide() must be used with an attribute declaration specifying a component class (attribute: 'Movie')\"\n    );\n  });\n\n  test('@consume()', async () => {\n    class Movie extends Component {\n      @consume() static Director;\n    }\n\n    class Director extends Component {\n      @consume() static Movie;\n    }\n\n    class Backend extends Component {\n      @provide() static Movie = Movie;\n      @provide() static Director = Director;\n    }\n\n    expect(Movie.getConsumedComponent('Director')).toBe(Director);\n    expect(Movie.Director).toBe(Director);\n    expect(Director.getConsumedComponent('Movie')).toBe(Movie);\n    expect(Director.Movie).toBe(Movie);\n\n    ((Backend, BackendMovie, BackendDirector) => {\n      class Movie extends BackendMovie {\n        @consume() static Director;\n      }\n\n      class Director extends BackendDirector {\n        @consume() static Movie;\n      }\n\n      class Frontend extends Backend {\n        @provide() static Movie = Movie;\n        @provide() static Director = Director;\n      }\n\n      expect(Movie.getConsumedComponent('Director')).toBe(Director);\n      expect(Movie.Director).toBe(Director);\n      expect(Director.getConsumedComponent('Movie')).toBe(Movie);\n      expect(Director.Movie).toBe(Movie);\n\n      return Frontend;\n    })(Backend, Movie, Director);\n\n    // The backend should not be affected by the frontend\n    expect(Movie.getConsumedComponent('Director')).toBe(Director);\n    expect(Movie.Director).toBe(Director);\n    expect(Director.getConsumedComponent('Movie')).toBe(Movie);\n    expect(Director.Movie).toBe(Movie);\n\n    expect(() => {\n      class Movie extends Component {\n        // @ts-expect-error\n        @consume() Director;\n      }\n\n      class Director extends Component {}\n\n      return Movie;\n    }).toThrow(\n      \"@consume() must be used inside a component class with as static attribute declaration (attribute: 'Director')\"\n    );\n\n    expect(() => {\n      class Director extends Component {}\n\n      class Movie extends Component {\n        @consume() static Director = Director;\n      }\n\n      return Movie;\n    }).toThrow(\n      \"@consume() must be used with an attribute declaration which does not specify any value (attribute: 'Director')\"\n    );\n  });\n});\n"
  },
  {
    "path": "packages/component/src/js-tests/jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"module\": \"CommonJS\",\n    \"checkJs\": true,\n    \"experimentalDecorators\": true\n  }\n}\n"
  },
  {
    "path": "packages/component/src/merging.test.ts",
    "content": "import {Component} from './component';\nimport {attribute, provide} from './decorators';\n\ndescribe('Merging', () => {\n  test('Simple component', async () => {\n    class Movie extends Component {\n      @attribute() static limit = 100;\n\n      @attribute() title!: string;\n      @attribute() tags!: string[];\n      @attribute() specs!: {duration?: number};\n    }\n\n    const MovieFork = Movie.fork();\n\n    MovieFork.limit = 500;\n\n    expect(Movie.limit).toBe(100);\n\n    Movie.merge(MovieFork);\n\n    expect(Movie.limit).toBe(500);\n\n    const movie = new Movie({title: 'Inception', tags: ['drama'], specs: {duration: 120}});\n\n    const movieFork = movie.fork();\n\n    movieFork.title = 'Inception 2';\n    movieFork.tags.push('action');\n    movieFork.specs.duration = 125;\n\n    expect(movie.title).toBe('Inception');\n    expect(movie.tags).toEqual(['drama']);\n    expect(movie.specs).toEqual({duration: 120});\n\n    movie.merge(movieFork);\n\n    expect(movie.title).toBe('Inception 2');\n    expect(movie.tags).not.toBe(movieFork.tags);\n    expect(movie.tags).toEqual(['drama', 'action']);\n    expect(movie.specs).not.toBe(movieFork.specs);\n    expect(movie.specs).toEqual({duration: 125});\n\n    movieFork.getAttribute('title').unsetValue();\n\n    expect(movie.getAttribute('title').isSet()).toBe(true);\n\n    movie.merge(movieFork);\n\n    expect(movie.getAttribute('title').isSet()).toBe(false);\n\n    movieFork.title = 'Inception 3';\n    movieFork.tags = ['action', 'adventure', 'sci-fi'];\n\n    movie.merge(movieFork, {attributeSelector: {title: true}});\n\n    expect(movie.title).toBe('Inception 3');\n    expect(movie.tags).toEqual(['drama', 'action']);\n  });\n\n  test('Referenced component', async () => {\n    class Director extends Component {\n      @attribute() name!: string;\n    }\n\n    class Movie extends Component {\n      @provide() static Director = Director;\n\n      @attribute() director!: Director;\n    }\n\n    const movie = new Movie({director: new Director({name: 'Christopher Nolan'})});\n\n    const director = movie.director;\n\n    const movieFork = movie.fork();\n\n    movieFork.director.name = 'Christopher Nolan 2';\n\n    expect(movie.director.name).toBe('Christopher Nolan');\n\n    movie.merge(movieFork);\n\n    expect(movie.director.name).toBe('Christopher Nolan 2');\n\n    // Although merged, the director should have kept its identity\n    expect(movie.director).toBe(director);\n  });\n});\n"
  },
  {
    "path": "packages/component/src/merging.ts",
    "content": "import {merge as simpleMerge, MergeOptions} from 'simple-forking';\n\nimport type {Component} from './component';\nimport {isComponentClass, isComponentInstance} from './utilities';\n\nexport {MergeOptions};\n\n/**\n * Deeply merge any type of forks including objects, arrays, and components (using Component's `merge()` [class method](https://layrjs.com/docs/v2/reference/component#merge-class-method) and [instance method](https://layrjs.com/docs/v2/reference/component#merge-instance-method)) into their original values.\n *\n * @param value An original value of any type.\n * @param valueFork A fork of `value`.\n *\n * @returns The original value.\n *\n * @example\n * ```\n * import {fork, merge} from '﹫layr/component';\n *\n * const data = {\n *   token: 'xyz123',\n *   timestamp: 1596600889609,\n *   movie: new Movie({title: 'Inception'})\n * };\n *\n * const dataFork = fork(data);\n * dataFork.token = 'xyz456';\n * dataFork.movie.title = 'Inception 2';\n *\n * data.token; // => 'xyz123'\n * data.movie.title; // => 'Inception'\n * merge(data, dataFork);\n * data.token; // => 'xyz456'\n * data.movie.title; // => 'Inception 2'\n * ```\n *\n * @category Merging\n */\nexport function merge(value: any, valueFork: any, options: MergeOptions = {}) {\n  const {\n    objectMerger: originalObjectMerger,\n    objectCloner: originalObjectCloner,\n    ...otherOptions\n  } = options;\n\n  const objectMerger = function (object: object, objectFork: object): object | void {\n    if (originalObjectMerger !== undefined) {\n      const mergedObject = originalObjectMerger(object, objectFork);\n\n      if (mergedObject !== undefined) {\n        return mergedObject;\n      }\n    }\n\n    if (isComponentClass(object)) {\n      return object.merge(objectFork as typeof Component, options);\n    }\n\n    if (isComponentInstance(object)) {\n      return object.merge(objectFork as Component, options);\n    }\n  };\n\n  const objectCloner = function (object: object): object | void {\n    if (originalObjectCloner !== undefined) {\n      const clonedObject = originalObjectCloner(object);\n\n      if (clonedObject !== undefined) {\n        return clonedObject;\n      }\n    }\n\n    if (isComponentClass(object)) {\n      return object.clone();\n    }\n\n    if (isComponentInstance(object)) {\n      return object.clone(options);\n    }\n  };\n\n  return simpleMerge(value, valueFork, {...otherOptions, objectMerger, objectCloner});\n}\n"
  },
  {
    "path": "packages/component/src/properties/attribute-selector.test.ts",
    "content": "import {Component} from '../component';\nimport {Attribute} from './attribute';\nimport {\n  createAttributeSelectorFromNames,\n  createAttributeSelectorFromAttributes,\n  getFromAttributeSelector,\n  setWithinAttributeSelector,\n  cloneAttributeSelector,\n  attributeSelectorsAreEqual,\n  attributeSelectorIncludes,\n  mergeAttributeSelectors,\n  intersectAttributeSelectors,\n  removeFromAttributeSelector,\n  iterateOverAttributeSelector,\n  pickFromAttributeSelector,\n  traverseAttributeSelector,\n  trimAttributeSelector,\n  normalizeAttributeSelector\n} from './attribute-selector';\nimport {attribute} from '../decorators';\n\ndescribe('AttributeSelector', () => {\n  test('createAttributeSelectorFromNames()', () => {\n    expect(createAttributeSelectorFromNames([])).toStrictEqual({});\n    expect(createAttributeSelectorFromNames(['title'])).toStrictEqual({title: true});\n    expect(createAttributeSelectorFromNames(['title', 'country'])).toStrictEqual({\n      title: true,\n      country: true\n    });\n  });\n\n  test('createAttributeSelectorFromAttributes()', () => {\n    const createAttributes = (names: string[]) =>\n      names.map((name) => (({getName: () => name} as unknown) as Attribute));\n\n    expect(createAttributeSelectorFromAttributes(createAttributes([]))).toStrictEqual({});\n    expect(createAttributeSelectorFromAttributes(createAttributes(['title']))).toStrictEqual({\n      title: true\n    });\n    expect(\n      createAttributeSelectorFromAttributes(createAttributes(['title', 'country']))\n    ).toStrictEqual({\n      title: true,\n      country: true\n    });\n  });\n\n  test('getFromAttributeSelector()', () => {\n    expect(getFromAttributeSelector(false, 'title')).toBe(false);\n    expect(getFromAttributeSelector(true, 'title')).toBe(true);\n\n    const attributeSelector = {title: true, director: {name: true}};\n\n    expect(getFromAttributeSelector(attributeSelector, 'title')).toBe(true);\n    expect(getFromAttributeSelector(attributeSelector, 'country')).toBe(false);\n    expect(getFromAttributeSelector(attributeSelector, 'director')).toStrictEqual({name: true});\n  });\n\n  test('setWithinAttributeSelector()', () => {\n    expect(setWithinAttributeSelector(false, 'title', false)).toBe(false);\n    expect(setWithinAttributeSelector(false, 'title', true)).toBe(false);\n    expect(setWithinAttributeSelector(true, 'title', false)).toBe(true);\n    expect(setWithinAttributeSelector(true, 'title', true)).toBe(true);\n    expect(setWithinAttributeSelector({}, 'title', false)).toStrictEqual({});\n    expect(setWithinAttributeSelector({}, 'title', true)).toStrictEqual({title: true});\n    expect(setWithinAttributeSelector({title: true}, 'title', false)).toStrictEqual({});\n    expect(setWithinAttributeSelector({title: true}, 'title', true)).toStrictEqual({title: true});\n    expect(setWithinAttributeSelector({title: true}, 'director', {})).toStrictEqual({\n      title: true,\n      director: {}\n    });\n    expect(setWithinAttributeSelector({title: true}, 'director', {name: true})).toStrictEqual({\n      title: true,\n      director: {name: true}\n    });\n  });\n\n  test('cloneAttributeSelector()', () => {\n    const attributeSelector = {title: true, director: {name: true}};\n    const clonedAttributeSelector = cloneAttributeSelector(attributeSelector);\n\n    expect(clonedAttributeSelector).not.toBe(attributeSelector);\n    expect(getFromAttributeSelector(clonedAttributeSelector, 'director')).not.toBe(\n      getFromAttributeSelector(attributeSelector, 'director')\n    );\n    expect(clonedAttributeSelector).toStrictEqual(attributeSelector);\n  });\n\n  test('attributeSelectorsAreEqual()', () => {\n    expect(attributeSelectorsAreEqual(false, false)).toBe(true);\n    expect(attributeSelectorsAreEqual(true, true)).toBe(true);\n    expect(attributeSelectorsAreEqual({}, {})).toBe(true);\n    expect(attributeSelectorsAreEqual({title: true}, {title: true})).toBe(true);\n    expect(\n      attributeSelectorsAreEqual({title: true, country: true}, {title: true, country: true})\n    ).toBe(true);\n    expect(\n      attributeSelectorsAreEqual(\n        {title: true, director: {name: true}},\n        {title: true, director: {name: true}}\n      )\n    ).toBe(true);\n\n    expect(attributeSelectorsAreEqual(false, true)).not.toBe(true);\n    expect(attributeSelectorsAreEqual(true, false)).not.toBe(true);\n    expect(attributeSelectorsAreEqual(false, {})).not.toBe(true);\n    expect(attributeSelectorsAreEqual({}, false)).not.toBe(true);\n    expect(attributeSelectorsAreEqual(true, {})).not.toBe(true);\n    expect(attributeSelectorsAreEqual({}, true)).not.toBe(true);\n    expect(attributeSelectorsAreEqual({title: true}, {})).not.toBe(true);\n    expect(attributeSelectorsAreEqual({}, {title: true})).not.toBe(true);\n    expect(attributeSelectorsAreEqual({title: true, country: true}, {title: true})).not.toBe(true);\n    expect(attributeSelectorsAreEqual({title: true}, {title: true, country: true})).not.toBe(true);\n    expect(\n      attributeSelectorsAreEqual(\n        {title: true, director: {name: true}},\n        {title: true, director: {country: true}}\n      )\n    ).not.toBe(true);\n  });\n\n  test('attributeSelectorIncludes()', () => {\n    expect(attributeSelectorIncludes(false, false)).toBe(true);\n    expect(attributeSelectorIncludes(true, false)).toBe(true);\n    expect(attributeSelectorIncludes(true, true)).toBe(true);\n    expect(attributeSelectorIncludes(true, {})).toBe(true);\n    expect(attributeSelectorIncludes(true, {title: true})).toBe(true);\n    expect(attributeSelectorIncludes({}, false)).toBe(true);\n    expect(attributeSelectorIncludes({}, {})).toBe(true);\n    expect(attributeSelectorIncludes({title: true}, false)).toBe(true);\n    expect(attributeSelectorIncludes({title: true}, {})).toBe(true);\n    expect(attributeSelectorIncludes({title: true}, {title: true})).toBe(true);\n    expect(attributeSelectorIncludes({title: true, country: true}, {title: true})).toBe(true);\n    expect(attributeSelectorIncludes({title: true, director: {name: true}}, {title: true})).toBe(\n      true\n    );\n    expect(\n      attributeSelectorIncludes({title: true, director: {name: true}}, {title: true, director: {}})\n    ).toBe(true);\n    expect(\n      attributeSelectorIncludes(\n        {title: true, director: {name: true}},\n        {title: true, director: {name: true}}\n      )\n    ).toBe(true);\n\n    expect(attributeSelectorIncludes(false, true)).not.toBe(true);\n    expect(attributeSelectorIncludes(false, {})).not.toBe(true);\n    expect(attributeSelectorIncludes(false, {title: true})).not.toBe(true);\n    expect(attributeSelectorIncludes({}, true)).not.toBe(true);\n    expect(attributeSelectorIncludes({title: true}, true)).not.toBe(true);\n    expect(attributeSelectorIncludes({}, {title: true})).not.toBe(true);\n    expect(attributeSelectorIncludes({title: true}, {country: true})).not.toBe(true);\n    expect(attributeSelectorIncludes({title: true}, {title: true, country: true})).not.toBe(true);\n    expect(attributeSelectorIncludes({title: true}, {title: true, director: {}})).not.toBe(true);\n    expect(\n      attributeSelectorIncludes({title: true, director: {}}, {title: true, director: {name: true}})\n    ).not.toBe(true);\n    expect(\n      attributeSelectorIncludes(\n        {title: true, director: {name: true}},\n        {title: true, director: {country: true}}\n      )\n    ).not.toBe(true);\n  });\n\n  test('mergeAttributeSelectors()', () => {\n    expect(mergeAttributeSelectors(false, false)).toBe(false);\n    expect(mergeAttributeSelectors(false, true)).toBe(true);\n    expect(mergeAttributeSelectors(true, false)).toBe(true);\n    expect(mergeAttributeSelectors(true, true)).toBe(true);\n    expect(mergeAttributeSelectors(true, {})).toBe(true);\n    expect(mergeAttributeSelectors(true, {title: true})).toBe(true);\n    expect(mergeAttributeSelectors(false, {})).toStrictEqual({});\n    expect(mergeAttributeSelectors(false, {title: true})).toStrictEqual({title: true});\n    expect(mergeAttributeSelectors({}, false)).toStrictEqual({});\n    expect(mergeAttributeSelectors({}, true)).toBe(true);\n    expect(mergeAttributeSelectors({}, {})).toStrictEqual({});\n    expect(mergeAttributeSelectors({title: true}, false)).toStrictEqual({title: true});\n    expect(mergeAttributeSelectors({title: true}, {})).toStrictEqual({title: true});\n    expect(mergeAttributeSelectors({title: true}, {title: true})).toStrictEqual({title: true});\n    expect(mergeAttributeSelectors({title: true}, {country: true})).toStrictEqual({\n      title: true,\n      country: true\n    });\n    expect(\n      mergeAttributeSelectors({title: true, director: {name: true}}, {title: true, director: true})\n    ).toStrictEqual({title: true, director: true});\n    expect(\n      mergeAttributeSelectors({title: true, director: true}, {title: true, director: {name: true}})\n    ).toStrictEqual({title: true, director: true});\n    expect(\n      mergeAttributeSelectors(\n        {title: true, director: {name: true}},\n        {title: true, director: {country: true}}\n      )\n    ).toStrictEqual({title: true, director: {name: true, country: true}});\n  });\n\n  test('intersectAttributeSelectors()', () => {\n    expect(intersectAttributeSelectors(false, false)).toBe(false);\n    expect(intersectAttributeSelectors(false, true)).toBe(false);\n    expect(intersectAttributeSelectors(true, false)).toBe(false);\n    expect(intersectAttributeSelectors(true, true)).toBe(true);\n    expect(intersectAttributeSelectors(true, {})).toStrictEqual({});\n    expect(intersectAttributeSelectors(true, {title: true})).toStrictEqual({title: true});\n    expect(intersectAttributeSelectors(false, {})).toBe(false);\n    expect(intersectAttributeSelectors(false, {title: true})).toBe(false);\n    expect(intersectAttributeSelectors({}, false)).toBe(false);\n    expect(intersectAttributeSelectors({}, true)).toStrictEqual({});\n    expect(intersectAttributeSelectors({}, {})).toStrictEqual({});\n    expect(intersectAttributeSelectors({title: true}, false)).toBe(false);\n    expect(intersectAttributeSelectors({title: true}, {})).toStrictEqual({});\n    expect(intersectAttributeSelectors({title: true}, {title: true})).toStrictEqual({title: true});\n    expect(intersectAttributeSelectors({title: true}, {country: true})).toStrictEqual({});\n    expect(\n      intersectAttributeSelectors(\n        {title: true, director: {name: true}},\n        {title: true, director: true}\n      )\n    ).toStrictEqual({title: true, director: {name: true}});\n    expect(\n      intersectAttributeSelectors(\n        {title: true, director: true},\n        {title: true, director: {name: true}}\n      )\n    ).toStrictEqual({title: true, director: {name: true}});\n    expect(\n      intersectAttributeSelectors(\n        {title: true, director: {name: true}},\n        {title: true, director: {country: true}}\n      )\n    ).toStrictEqual({title: true, director: {}});\n  });\n\n  test('removeFromAttributeSelector()', () => {\n    expect(removeFromAttributeSelector(false, false)).toBe(false);\n    expect(removeFromAttributeSelector(false, true)).toBe(false);\n    expect(removeFromAttributeSelector(true, false)).toBe(true);\n    expect(removeFromAttributeSelector(true, true)).toBe(false);\n    expect(() => removeFromAttributeSelector(true, {})).toThrow(\n      \"Cannot remove an 'object' attribute selector from a 'true' attribute selector\"\n    );\n    expect(removeFromAttributeSelector(false, {})).toBe(false);\n    expect(removeFromAttributeSelector({}, false)).toStrictEqual({});\n    expect(removeFromAttributeSelector({}, true)).toBe(false);\n    expect(removeFromAttributeSelector({}, {})).toStrictEqual({});\n    expect(removeFromAttributeSelector({title: true}, {})).toStrictEqual({title: true});\n    expect(removeFromAttributeSelector({title: true}, {title: false})).toStrictEqual({title: true});\n    expect(removeFromAttributeSelector({title: true}, {title: true})).toStrictEqual({});\n    expect(removeFromAttributeSelector({title: true}, {country: true})).toStrictEqual({\n      title: true\n    });\n    expect(\n      removeFromAttributeSelector({title: true, director: {name: true}}, {director: true})\n    ).toStrictEqual({\n      title: true\n    });\n    expect(() =>\n      removeFromAttributeSelector(\n        {title: true, director: true},\n        {title: true, director: {name: true}}\n      )\n    ).toThrow(\"Cannot remove an 'object' attribute selector from a 'true' attribute selector\");\n    expect(\n      removeFromAttributeSelector(\n        {title: true, director: {name: true}},\n        {title: true, director: {name: true}}\n      )\n    ).toStrictEqual({director: {}});\n  });\n\n  test('iterateOverAttributeSelector()', () => {\n    expect(Array.from(iterateOverAttributeSelector({}))).toStrictEqual([]);\n\n    expect(Array.from(iterateOverAttributeSelector({title: undefined}))).toStrictEqual([]);\n\n    expect(Array.from(iterateOverAttributeSelector({title: false}))).toStrictEqual([]);\n\n    expect(Array.from(iterateOverAttributeSelector({title: true}))).toStrictEqual([\n      ['title', true]\n    ]);\n\n    expect(Array.from(iterateOverAttributeSelector({specs: {duration: true}}))).toStrictEqual([\n      ['specs', {duration: true}]\n    ]);\n\n    expect(\n      Array.from(iterateOverAttributeSelector({title: true, country: false, director: true}))\n    ).toStrictEqual([\n      ['title', true],\n      ['director', true]\n    ]);\n  });\n\n  test('pickFromAttributeSelector()', () => {\n    class Organization extends Component {\n      @attribute() name?: string;\n      @attribute() country?: string;\n    }\n\n    const organization = Organization.instantiate();\n    organization.name = 'Paradise Inc.';\n\n    const createdOn = new Date();\n\n    const person = {\n      id: 'abc123',\n      email: 'hi@hello.com',\n      emailIsConfirmed: true,\n      reference: 123,\n      tags: ['admin', 'creator'],\n      location: undefined,\n      organization,\n      friends: [\n        {__component: 'person', id: 'def456', reference: 456},\n        {__component: 'person', id: 'ghi789', reference: 789}\n      ],\n      matrix: [\n        [\n          {name: 'a', value: 111},\n          {name: 'b', value: 222}\n        ],\n        [\n          {name: 'c', value: 333},\n          {name: 'b', value: 444}\n        ]\n      ],\n      createdOn\n    };\n\n    expect(pickFromAttributeSelector(person, true)).toStrictEqual(person);\n    expect(pickFromAttributeSelector(person, {})).toStrictEqual({});\n\n    expect(\n      pickFromAttributeSelector(person, {\n        id: true,\n        emailIsConfirmed: true,\n        reference: true,\n        createdOn: true\n      })\n    ).toStrictEqual({\n      id: 'abc123',\n      emailIsConfirmed: true,\n      reference: 123,\n      createdOn\n    });\n\n    expect(pickFromAttributeSelector(person, {tags: true})).toStrictEqual({\n      tags: ['admin', 'creator']\n    });\n\n    expect(pickFromAttributeSelector(person, {organization: true})).toStrictEqual({organization});\n\n    expect(pickFromAttributeSelector(person, {organization: {name: true}})).toStrictEqual({\n      organization: {name: 'Paradise Inc.'}\n    });\n\n    expect(pickFromAttributeSelector(person, {friends: true})).toStrictEqual({\n      friends: [\n        {__component: 'person', id: 'def456', reference: 456},\n        {__component: 'person', id: 'ghi789', reference: 789}\n      ]\n    });\n\n    expect(pickFromAttributeSelector(person, {friends: {id: true}})).toStrictEqual({\n      friends: [{id: 'def456'}, {id: 'ghi789'}]\n    });\n\n    expect(\n      pickFromAttributeSelector(\n        person,\n        {friends: {reference: true}},\n        {includeAttributeNames: ['__component']}\n      )\n    ).toStrictEqual({\n      friends: [\n        {__component: 'person', reference: 456},\n        {__component: 'person', reference: 789}\n      ]\n    });\n\n    expect(pickFromAttributeSelector(person, {matrix: {value: true}})).toStrictEqual({\n      matrix: [\n        [{value: 111}, {value: 222}],\n        [{value: 333}, {value: 444}]\n      ]\n    });\n\n    expect(pickFromAttributeSelector(undefined, {location: true})).toBeUndefined();\n\n    expect(pickFromAttributeSelector(person, {location: true})).toStrictEqual({\n      location: undefined\n    });\n\n    expect(pickFromAttributeSelector(person, {location: {country: true}})).toStrictEqual({\n      location: undefined\n    });\n\n    expect(() => pickFromAttributeSelector(null, {id: true})).toThrow(\n      \"Cannot pick attributes from a value that is not a component, a plain object, or an array (value type: 'null')\"\n    );\n    expect(() => pickFromAttributeSelector('abc123', {id: true})).toThrow(\n      \"Cannot pick attributes from a value that is not a component, a plain object, or an array (value type: 'string')\"\n    );\n    expect(() => pickFromAttributeSelector(createdOn, {id: true})).toThrow(\n      \"Cannot pick attributes from a value that is not a component, a plain object, or an array (value type: 'Date')\"\n    );\n    expect(() => pickFromAttributeSelector(person, {reference: {value: true}})).toThrow(\n      \"Cannot pick attributes from a value that is not a component, a plain object, or an array (value type: 'number')\"\n    );\n    expect(() => pickFromAttributeSelector(person, false)).toThrow(\n      \"Cannot pick attributes from a value when the specified attribute selector is 'false'\"\n    );\n    expect(() => pickFromAttributeSelector(person, {organization: {country: true}})).toThrow(\n      \"Cannot get the value of an unset attribute (attribute: 'Organization.prototype.country')\"\n    );\n    expect(() => pickFromAttributeSelector(person, {organization: {city: true}})).toThrow(\n      \"The attribute 'city' is missing (component: 'Organization')\"\n    );\n  });\n\n  test('traverseAttributeSelector()', () => {\n    class Organization extends Component {\n      @attribute() name?: string;\n      @attribute() country?: string;\n    }\n\n    const organization = Organization.instantiate();\n    organization.name = 'Paradise Inc.';\n\n    const createdOn = new Date();\n\n    const person = {\n      id: 'abc123',\n      email: 'hi@hello.com',\n      emailIsConfirmed: true,\n      reference: 123,\n      tags: ['admin', 'creator'],\n      location: undefined,\n      organization,\n      friends: [\n        {firstName: 'Bob', lastName: 'Sinclair'},\n        {firstName: 'John', lastName: 'Thomas'}\n      ],\n      matrix: [\n        [\n          {name: 'a', value: 111},\n          {name: 'b', value: 222}\n        ],\n        [\n          {name: 'c', value: 333},\n          {name: 'b', value: 444}\n        ]\n      ],\n      createdOn\n    };\n\n    const runTraverse = function (value: any, attributeSelector?: any, options?: any) {\n      const results: any[] = [];\n\n      traverseAttributeSelector(\n        value,\n        attributeSelector,\n        function (value, attributeSelector, {name, object, isArray}) {\n          results.push({\n            value,\n            attributeSelector,\n            name,\n            object,\n            ...(isArray && {isArray: true})\n          });\n        },\n        options\n      );\n\n      return results;\n    };\n\n    expect(runTraverse(person, true)).toStrictEqual([\n      {value: person, attributeSelector: true, name: undefined, object: undefined}\n    ]);\n\n    expect(runTraverse(person, false)).toStrictEqual([]);\n\n    expect(\n      runTraverse(person, {\n        id: true,\n        emailIsConfirmed: true,\n        reference: true,\n        createdOn: true\n      })\n    ).toStrictEqual([\n      {value: 'abc123', attributeSelector: true, name: 'id', object: person},\n      {value: true, attributeSelector: true, name: 'emailIsConfirmed', object: person},\n      {value: 123, attributeSelector: true, name: 'reference', object: person},\n      {value: createdOn, attributeSelector: true, name: 'createdOn', object: person}\n    ]);\n\n    expect(runTraverse(person, {tags: true})).toStrictEqual([\n      {value: ['admin', 'creator'], attributeSelector: true, name: 'tags', object: person}\n    ]);\n\n    expect(runTraverse(person, {organization: true})).toStrictEqual([\n      {value: organization, attributeSelector: true, name: 'organization', object: person}\n    ]);\n\n    expect(runTraverse(person, {organization: {name: true, country: true}})).toStrictEqual([\n      {value: 'Paradise Inc.', attributeSelector: true, name: 'name', object: person.organization}\n    ]);\n\n    expect(runTraverse(person, {friends: true})).toStrictEqual([\n      {value: person.friends, attributeSelector: true, name: 'friends', object: person}\n    ]);\n\n    expect(runTraverse(person, {friends: {firstName: true}})).toStrictEqual([\n      {value: 'Bob', attributeSelector: true, name: 'firstName', object: person.friends[0]},\n      {value: 'John', attributeSelector: true, name: 'firstName', object: person.friends[1]}\n    ]);\n\n    expect(runTraverse(person, {matrix: {value: true}})).toStrictEqual([\n      {value: 111, attributeSelector: true, name: 'value', object: person.matrix[0][0]},\n      {value: 222, attributeSelector: true, name: 'value', object: person.matrix[0][1]},\n      {value: 333, attributeSelector: true, name: 'value', object: person.matrix[1][0]},\n      {value: 444, attributeSelector: true, name: 'value', object: person.matrix[1][1]}\n    ]);\n\n    expect(runTraverse(undefined, {location: true})).toStrictEqual([\n      {value: undefined, attributeSelector: {location: true}, name: undefined, object: undefined}\n    ]);\n\n    expect(runTraverse(person, {location: true})).toStrictEqual([\n      {value: undefined, attributeSelector: true, name: 'location', object: person}\n    ]);\n\n    expect(runTraverse(person, {location: {country: true}})).toStrictEqual([\n      {\n        value: undefined,\n        attributeSelector: {country: true},\n        name: 'location',\n        object: person\n      }\n    ]);\n\n    expect(\n      runTraverse(\n        person,\n        {organization: {name: true}},\n        {includeSubtrees: true, includeLeafs: false}\n      )\n    ).toStrictEqual([\n      {\n        value: person.organization,\n        attributeSelector: {name: true},\n        name: 'organization',\n        object: person\n      }\n    ]);\n\n    expect(\n      runTraverse(person, {organization: {name: true}}, {includeSubtrees: true, includeLeafs: true})\n    ).toStrictEqual([\n      {\n        value: person.organization,\n        attributeSelector: {name: true},\n        name: 'organization',\n        object: person\n      },\n      {value: 'Paradise Inc.', attributeSelector: true, name: 'name', object: person.organization}\n    ]);\n\n    expect(\n      runTraverse(\n        person,\n        {friends: {firstName: true}},\n        {includeSubtrees: true, includeLeafs: false}\n      )\n    ).toStrictEqual([\n      {\n        value: person.friends[0],\n        attributeSelector: {firstName: true},\n        name: 'friends',\n        object: person,\n        isArray: true\n      },\n      {\n        value: person.friends[1],\n        attributeSelector: {firstName: true},\n        name: 'friends',\n        object: person,\n        isArray: true\n      }\n    ]);\n\n    expect(runTraverse(person, true, {includeSubtrees: true, includeLeafs: false})).toStrictEqual(\n      []\n    );\n\n    expect(() => runTraverse(null, {id: true})).toThrow(\n      \"Cannot traverse attributes from a value that is not a component, a plain object, or an array (value type: 'null')\"\n    );\n    expect(() => runTraverse('abc123', {id: true})).toThrow(\n      \"Cannot traverse attributes from a value that is not a component, a plain object, or an array (value type: 'string')\"\n    );\n    expect(() => runTraverse(createdOn, {id: true})).toThrow(\n      \"Cannot traverse attributes from a value that is not a component, a plain object, or an array (value type: 'Date')\"\n    );\n    expect(() => runTraverse(person, {reference: {value: true}})).toThrow(\n      \"Cannot traverse attributes from a value that is not a component, a plain object, or an array (value type: 'number')\"\n    );\n    expect(() => runTraverse(person, {organization: {city: true}})).toThrow(\n      \"The attribute 'city' is missing (component: 'Organization')\"\n    );\n  });\n\n  test('trimAttributeSelector()', () => {\n    expect(trimAttributeSelector(true)).toBe(true);\n    expect(trimAttributeSelector(false)).toBe(false);\n    expect(trimAttributeSelector({})).toBe(false);\n    expect(trimAttributeSelector({title: false})).toBe(false);\n    expect(trimAttributeSelector({director: {}})).toBe(false);\n    expect(trimAttributeSelector({director: {name: false}})).toBe(false);\n\n    expect(trimAttributeSelector({title: true})).toStrictEqual({title: true});\n    expect(trimAttributeSelector({title: true, director: {name: false}})).toStrictEqual({\n      title: true\n    });\n    expect(trimAttributeSelector({title: false, director: {name: true}})).toStrictEqual({\n      director: {name: true}\n    });\n  });\n\n  test('normalizeAttributeSelector()', () => {\n    expect(normalizeAttributeSelector(true)).toBe(true);\n    expect(normalizeAttributeSelector(false)).toBe(false);\n    expect(normalizeAttributeSelector(undefined)).toBe(false);\n    expect(normalizeAttributeSelector({})).toStrictEqual({});\n    expect(normalizeAttributeSelector({title: true, director: {name: true}})).toStrictEqual({\n      title: true,\n      director: {name: true}\n    });\n\n    class Movie {}\n\n    expect(() => normalizeAttributeSelector(null)).toThrow(\n      \"Expected a valid attribute selector, but received a value of type 'null'\"\n    );\n    expect(() => normalizeAttributeSelector(1)).toThrow(\n      \"Expected a valid attribute selector, but received a value of type 'number'\"\n    );\n    expect(() => normalizeAttributeSelector('a')).toThrow(\n      \"Expected a valid attribute selector, but received a value of type 'string'\"\n    );\n    expect(() => normalizeAttributeSelector(['a'])).toThrow(\n      \"Expected a valid attribute selector, but received a value of type 'Array'\"\n    );\n    expect(() => normalizeAttributeSelector(() => {})).toThrow(\n      \"Expected a valid attribute selector, but received a value of type 'Function'\"\n    );\n    expect(() => normalizeAttributeSelector(new Date())).toThrow(\n      \"Expected a valid attribute selector, but received a value of type 'Date'\"\n    );\n    expect(() => normalizeAttributeSelector(new Movie())).toThrow(\n      \"Expected a valid attribute selector, but received a value of type 'Movie'\"\n    );\n  });\n});\n"
  },
  {
    "path": "packages/component/src/properties/attribute-selector.ts",
    "content": "import {\n  hasOwnProperty,\n  getTypeOf,\n  isPlainObject,\n  PlainObject,\n  assertIsFunction\n} from 'core-helpers';\nimport omit from 'lodash/omit';\nimport cloneDeep from 'lodash/cloneDeep';\nimport isEmpty from 'lodash/isEmpty';\n\nimport type {Attribute} from './attribute';\nimport {isComponentClassOrInstance} from '../utilities';\n\nexport type AttributeSelector = boolean | PlainObject;\n\n/**\n * @typedef AttributeSelector\n *\n * An `AttributeSelector` allows you to select some attributes of a component.\n *\n * The simplest `AttributeSelector` is `true`, which means that all the attributes are selected.\n\n * Another possible `AttributeSelector` is `false`, which means that no attributes are selected.\n *\n * To select some specific attributes, you can use a plain object where:\n *\n * * The keys are the name of the attributes you want to select.\n * * The values are a boolean or a nested object to select some attributes of a nested component.\n *\n * **Examples:**\n *\n * ```\n * // Selects all the attributes\n * true\n *\n * // Excludes all the attributes\n * false\n *\n * // Selects `title`\n * {title: true}\n *\n * // Selects also `title` (`summary` is not selected)\n * {title: true, summary: false}\n *\n * // Selects `title` and `summary`\n * {title: true, summary: true}\n *\n * // Selects `title`, `movieDetails.duration`, and `movieDetails.aspectRatio`\n * {\n *   title: true,\n *   movieDetails: {\n *     duration: true,\n *     aspectRatio: true\n *   }\n * }\n * ```\n */\n\n/**\n * Creates an `AttributeSelector` from the specified names.\n *\n * @param names An array of strings.\n *\n * @returns An `AttributeSelector`.\n *\n * @example\n * ```\n * createAttributeSelectorFromNames(['title', 'summary']);\n * // => {title: true, summary: true}\n * ```\n *\n * @category Functions\n */\nexport function createAttributeSelectorFromNames(names: string[]) {\n  const attributeSelector: AttributeSelector = {};\n\n  for (const name of names) {\n    attributeSelector[name] = true;\n  }\n\n  return attributeSelector;\n}\n\n/**\n * Creates an `AttributeSelector` from an attribute iterator.\n *\n * @param attributes An [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) iterator.\n *\n * @returns An `AttributeSelector`.\n *\n * @example\n * ```\n * createAttributeSelectorFromAttributes(Movie.prototype.getAttributes());\n * // => {title: true, summary: true, movieDetails: true}\n * ```\n *\n * @category Functions\n */\nexport function createAttributeSelectorFromAttributes(attributes: Iterable<Attribute>) {\n  const attributeSelector: AttributeSelector = {};\n\n  for (const attribute of attributes) {\n    attributeSelector[attribute.getName()] = true;\n  }\n\n  return attributeSelector;\n}\n\n/**\n * Gets an entry of an `AttributeSelector`.\n *\n * @param attributeSelector An `AttributeSelector`.\n * @param name The name of the entry to get.\n *\n * @returns An `AttributeSelector`.\n *\n * @example\n * ```\n * getFromAttributeSelector(true, 'title');\n * // => true\n *\n * getFromAttributeSelector(false, 'title');\n * // => false\n *\n * getFromAttributeSelector({title: true}, 'title');\n * // => true\n *\n * getFromAttributeSelector({title: true}, 'summary');\n * // => false\n *\n * getFromAttributeSelector({movieDetails: {duration: true}}, 'movieDetails');\n * // => {duration: true}\n * ```\n *\n * @category Functions\n */\nexport function getFromAttributeSelector(\n  attributeSelector: AttributeSelector,\n  name: string\n): AttributeSelector {\n  attributeSelector = normalizeAttributeSelector(attributeSelector);\n\n  if (typeof attributeSelector === 'boolean') {\n    return attributeSelector;\n  }\n\n  return normalizeAttributeSelector(attributeSelector[name]);\n}\n\n/**\n * Returns an `AttributeSelector` where an entry of the specified `AttributeSelector` is set with another `AttributeSelector`.\n *\n * @param attributeSelector An `AttributeSelector`.\n * @param name The name of the entry to set.\n * @param subattributeSelector Another `AttributeSelector`.\n *\n * @returns A new `AttributeSelector`.\n *\n * @example\n * ```\n * setWithinAttributeSelector({title: true}, 'summary', true);\n * // => {title: true, summary: true}\n *\n * setWithinAttributeSelector({title: true}, 'summary', false);\n * // => {title: true}\n *\n * setWithinAttributeSelector({title: true, summary: true}, 'summary', false);\n * // => {title: true}\n *\n * setWithinAttributeSelector({title: true}, 'movieDetails', {duration: true});\n * // => {title: true, movieDetails: {duration: true}}\n * ```\n *\n * @category Functions\n */\nexport function setWithinAttributeSelector(\n  attributeSelector: AttributeSelector,\n  name: string,\n  subattributeSelector: AttributeSelector\n): AttributeSelector {\n  attributeSelector = normalizeAttributeSelector(attributeSelector);\n\n  if (typeof attributeSelector === 'boolean') {\n    return attributeSelector;\n  }\n\n  subattributeSelector = normalizeAttributeSelector(subattributeSelector);\n\n  if (subattributeSelector === false) {\n    return omit(attributeSelector, name);\n  }\n\n  return {...attributeSelector, [name]: subattributeSelector};\n}\n\n/**\n * Clones an `AttributeSelector`.\n *\n * @param attributeSelector An `AttributeSelector`.\n *\n * @returns A new `AttributeSelector`.\n *\n * @example\n * ```\n * cloneAttributeSelector(true);\n * // => true\n *\n * cloneAttributeSelector(false);\n * // => false\n *\n * cloneAttributeSelector({title: true, movieDetails: {duration: true});\n * // => {title: true, movieDetails: {duration: true}\n * ```\n *\n * @category Functions\n */\nexport function cloneAttributeSelector(attributeSelector: AttributeSelector) {\n  return cloneDeep(attributeSelector);\n}\n\n/**\n * Returns whether an `AttributeSelector` is equal to another `AttributeSelector`.\n *\n * @param attributeSelector An `AttributeSelector`.\n * @param otherAttributeSelector Another `AttributeSelector`.\n *\n * @returns A boolean.\n *\n * @example\n * ```\n * attributeSelectorsAreEqual({title: true}, {title: true});\n * // => true\n *\n * attributeSelectorsAreEqual({title: true, summary: false}, {title: true});\n * // => true\n *\n * attributeSelectorsAreEqual({title: true}, {summary: true});\n * // => false\n * ```\n *\n * @category Functions\n */\nexport function attributeSelectorsAreEqual(\n  attributeSelector: AttributeSelector,\n  otherAttributeSelector: AttributeSelector\n) {\n  return (\n    attributeSelector === otherAttributeSelector ||\n    (attributeSelectorIncludes(attributeSelector, otherAttributeSelector) &&\n      attributeSelectorIncludes(otherAttributeSelector, attributeSelector))\n  );\n}\n\n/**\n * Returns whether an `AttributeSelector` includes another `AttributeSelector`.\n *\n * @param attributeSelector An `AttributeSelector`.\n * @param otherAttributeSelector Another `AttributeSelector`.\n *\n * @returns A boolean.\n *\n * @example\n * ```\n * attributeSelectorIncludes({title: true}, {title: true});\n * // => true\n *\n * attributeSelectorIncludes({title: true, summary: true}, {title: true});\n * // => true\n *\n * attributeSelectorIncludes({title: true}, {summary: true});\n * // => false\n * ```\n *\n * @category Functions\n */\nexport function attributeSelectorIncludes(\n  attributeSelector: AttributeSelector,\n  otherAttributeSelector: AttributeSelector\n) {\n  attributeSelector = normalizeAttributeSelector(attributeSelector);\n  otherAttributeSelector = normalizeAttributeSelector(otherAttributeSelector);\n\n  if (attributeSelector === otherAttributeSelector) {\n    return true;\n  }\n\n  if (typeof attributeSelector === 'boolean') {\n    return attributeSelector;\n  }\n\n  if (typeof otherAttributeSelector === 'boolean') {\n    return !otherAttributeSelector;\n  }\n\n  for (const [name, otherSubattributeSelector] of Object.entries(otherAttributeSelector)) {\n    const subattributeSelector = attributeSelector[name];\n\n    if (!attributeSelectorIncludes(subattributeSelector, otherSubattributeSelector)) {\n      return false;\n    }\n  }\n\n  return true;\n}\n\n/**\n * Returns an `AttributeSelector` which is the result of merging an `AttributeSelector` with another `AttributeSelector`.\n *\n * @param attributeSelector An `AttributeSelector`.\n * @param otherAttributeSelector Another `AttributeSelector`.\n *\n * @returns A new `AttributeSelector`.\n *\n * @example\n * ```\n * mergeAttributeSelectors({title: true}, {title: true});\n * // => {title: true}\n *\n * mergeAttributeSelectors({title: true}, {summary: true});\n * // => {title: true, summary: true}\n *\n * mergeAttributeSelectors({title: true, summary: true}, {summary: false});\n * // => {title: true}\n * ```\n *\n * @category Functions\n */\nexport function mergeAttributeSelectors(\n  attributeSelector: AttributeSelector,\n  otherAttributeSelector: AttributeSelector\n): AttributeSelector {\n  attributeSelector = normalizeAttributeSelector(attributeSelector);\n  otherAttributeSelector = normalizeAttributeSelector(otherAttributeSelector);\n\n  if (attributeSelector === true) {\n    return true;\n  }\n\n  if (attributeSelector === false) {\n    return otherAttributeSelector;\n  }\n\n  if (otherAttributeSelector === true) {\n    return true;\n  }\n\n  if (otherAttributeSelector === false) {\n    return attributeSelector;\n  }\n\n  for (const [name, otherSubattributeSelector] of Object.entries(otherAttributeSelector)) {\n    const subattributeSelector = (attributeSelector as PlainObject)[name];\n\n    attributeSelector = setWithinAttributeSelector(\n      attributeSelector,\n      name,\n      mergeAttributeSelectors(subattributeSelector, otherSubattributeSelector)\n    );\n  }\n\n  return attributeSelector;\n}\n\n/**\n * Returns an `AttributeSelector` which is the result of the intersection of an `AttributeSelector` with another `AttributeSelector`.\n *\n * @param attributeSelector An `AttributeSelector`.\n * @param otherAttributeSelector Another `AttributeSelector`.\n *\n * @returns A new `AttributeSelector`.\n *\n * @example\n * ```\n * intersectAttributeSelectors({title: true, summary: true}, {title: true});\n * // => {title: true}\n *\n * intersectAttributeSelectors({title: true}, {summary: true});\n * // => {}\n * ```\n *\n * @category Functions\n */\nexport function intersectAttributeSelectors(\n  attributeSelector: AttributeSelector,\n  otherAttributeSelector: AttributeSelector\n): AttributeSelector {\n  attributeSelector = normalizeAttributeSelector(attributeSelector);\n  otherAttributeSelector = normalizeAttributeSelector(otherAttributeSelector);\n\n  if (attributeSelector === false || otherAttributeSelector === false) {\n    return false;\n  }\n\n  if (attributeSelector === true) {\n    return otherAttributeSelector;\n  }\n\n  if (otherAttributeSelector === true) {\n    return attributeSelector;\n  }\n\n  let intersectedAttributeSelector = {};\n\n  for (const [name, otherSubattributeSelector] of Object.entries(otherAttributeSelector)) {\n    const subattributeSelector = (attributeSelector as PlainObject)[name];\n\n    intersectedAttributeSelector = setWithinAttributeSelector(\n      intersectedAttributeSelector,\n      name,\n      intersectAttributeSelectors(subattributeSelector, otherSubattributeSelector)\n    );\n  }\n\n  return intersectedAttributeSelector;\n}\n\n/**\n * Returns an `AttributeSelector` which is the result of removing an `AttributeSelector` from another `AttributeSelector`.\n *\n * @param attributeSelector An `AttributeSelector`.\n * @param otherAttributeSelector Another `AttributeSelector`.\n *\n * @returns A new `AttributeSelector`.\n *\n * @example\n * ```\n * removeFromAttributeSelector({title: true, summary: true}, {summary: true});\n * // => {title: true}\n *\n * removeFromAttributeSelector({title: true}, {title: true});\n * // => {}\n * ```\n *\n * @category Functions\n */\nexport function removeFromAttributeSelector(\n  attributeSelector: AttributeSelector,\n  otherAttributeSelector: AttributeSelector\n): AttributeSelector {\n  attributeSelector = normalizeAttributeSelector(attributeSelector);\n  otherAttributeSelector = normalizeAttributeSelector(otherAttributeSelector);\n\n  if (otherAttributeSelector === true) {\n    return false;\n  }\n\n  if (otherAttributeSelector === false) {\n    return attributeSelector;\n  }\n\n  if (attributeSelector === true) {\n    throw new Error(\n      \"Cannot remove an 'object' attribute selector from a 'true' attribute selector\"\n    );\n  }\n\n  if (attributeSelector === false) {\n    return false;\n  }\n\n  for (const [name, otherSubattributeSelector] of Object.entries(otherAttributeSelector)) {\n    const subattributeSelector = (attributeSelector as PlainObject)[name];\n\n    attributeSelector = setWithinAttributeSelector(\n      attributeSelector,\n      name,\n      removeFromAttributeSelector(subattributeSelector, otherSubattributeSelector)\n    );\n  }\n\n  return attributeSelector;\n}\n\nexport function iterateOverAttributeSelector(attributeSelector: AttributeSelector) {\n  return {\n    *[Symbol.iterator]() {\n      for (const [name, subattributeSelector] of Object.entries(attributeSelector)) {\n        const normalizedSubattributeSelector = normalizeAttributeSelector(subattributeSelector);\n\n        if (normalizedSubattributeSelector !== false) {\n          yield [name, normalizedSubattributeSelector] as [string, AttributeSelector];\n        }\n      }\n    }\n  };\n}\n\ntype PickFromAttributeSelectorResult<Value> = Value extends Array<infer Element>\n  ? Array<PickFromAttributeSelectorResult<Element>>\n  : Value extends object\n  ? object\n  : Value;\n\nexport function pickFromAttributeSelector<Value>(\n  value: Value,\n  attributeSelector: AttributeSelector,\n  options?: {includeAttributeNames?: string[]}\n): PickFromAttributeSelectorResult<Value>;\nexport function pickFromAttributeSelector(\n  value: unknown,\n  attributeSelector: AttributeSelector,\n  options: {includeAttributeNames?: string[]} = {}\n) {\n  attributeSelector = normalizeAttributeSelector(attributeSelector);\n\n  if (attributeSelector === false) {\n    throw new Error(\n      `Cannot pick attributes from a value when the specified attribute selector is 'false'`\n    );\n  }\n\n  const {includeAttributeNames = []} = options;\n\n  return _pick(value, attributeSelector, {includeAttributeNames});\n}\n\nfunction _pick(\n  value: unknown,\n  attributeSelector: AttributeSelector,\n  {includeAttributeNames}: {includeAttributeNames: string[]}\n): unknown {\n  if (attributeSelector === true) {\n    return value;\n  }\n\n  if (value === undefined) {\n    return undefined;\n  }\n\n  if (Array.isArray(value)) {\n    const array = value;\n\n    return array.map((value) => _pick(value, attributeSelector, {includeAttributeNames}));\n  }\n\n  const isComponent = isComponentClassOrInstance(value);\n\n  if (!(isComponent || isPlainObject(value))) {\n    throw new Error(\n      `Cannot pick attributes from a value that is not a component, a plain object, or an array (value type: '${getTypeOf(\n        value\n      )}')`\n    );\n  }\n\n  const componentOrObject = value as PlainObject;\n\n  const result: PlainObject = {};\n\n  if (!isComponent) {\n    for (const name of includeAttributeNames) {\n      if (hasOwnProperty(componentOrObject, name)) {\n        result[name] = componentOrObject[name];\n      }\n    }\n  }\n\n  for (const [name, subattributeSelector] of iterateOverAttributeSelector(attributeSelector)) {\n    const value = isComponent\n      ? componentOrObject.getAttribute(name).getValue()\n      : componentOrObject[name];\n\n    result[name] = _pick(value, subattributeSelector, {includeAttributeNames});\n  }\n\n  return result;\n}\n\ntype TraverseIteratee = (\n  value: any,\n  attributeSelector: AttributeSelector,\n  context: TraverseContext\n) => void;\n\ntype TraverseContext = {name?: string; object?: object; isArray?: boolean};\n\ntype TraverseOptions = {includeSubtrees?: boolean; includeLeafs?: boolean};\n\nexport function traverseAttributeSelector(\n  value: any,\n  attributeSelector: AttributeSelector,\n  iteratee: TraverseIteratee,\n  options: TraverseOptions = {}\n) {\n  attributeSelector = normalizeAttributeSelector(attributeSelector);\n\n  assertIsFunction(iteratee);\n\n  const {includeSubtrees = false, includeLeafs = true} = options;\n\n  if (attributeSelector === false) {\n    return;\n  }\n\n  _traverse(value, attributeSelector, iteratee, {\n    includeSubtrees,\n    includeLeafs,\n    _context: {},\n    _isDeep: false\n  });\n}\n\nfunction _traverse(\n  value: any,\n  attributeSelector: AttributeSelector,\n  iteratee: TraverseIteratee,\n  {\n    includeSubtrees,\n    includeLeafs,\n    _context,\n    _isDeep\n  }: TraverseOptions & {_context: TraverseContext; _isDeep: boolean}\n) {\n  if (attributeSelector === true || value === undefined) {\n    if (includeLeafs) {\n      iteratee(value, attributeSelector, _context);\n    }\n\n    return;\n  }\n\n  if (Array.isArray(value)) {\n    const array = value;\n\n    for (const value of array) {\n      _traverse(value, attributeSelector, iteratee, {\n        includeSubtrees,\n        includeLeafs,\n        _context: {..._context, isArray: true},\n        _isDeep\n      });\n    }\n\n    return;\n  }\n\n  const isComponent = isComponentClassOrInstance(value);\n\n  if (!(isComponent || isPlainObject(value))) {\n    throw new Error(\n      `Cannot traverse attributes from a value that is not a component, a plain object, or an array (value type: '${getTypeOf(\n        value\n      )}')`\n    );\n  }\n\n  const componentOrObject = value;\n\n  if (_isDeep && includeSubtrees) {\n    iteratee(componentOrObject, attributeSelector, _context);\n  }\n\n  for (const [name, subattributeSelector] of iterateOverAttributeSelector(attributeSelector)) {\n    if (isComponent && !componentOrObject.getAttribute(name).isSet()) {\n      continue;\n    }\n\n    const value = componentOrObject[name];\n\n    _traverse(value, subattributeSelector, iteratee, {\n      includeSubtrees,\n      includeLeafs,\n      _context: {name, object: componentOrObject},\n      _isDeep: true\n    });\n  }\n}\n\nexport function trimAttributeSelector(attributeSelector: AttributeSelector): AttributeSelector {\n  attributeSelector = normalizeAttributeSelector(attributeSelector);\n\n  if (typeof attributeSelector === 'boolean') {\n    return attributeSelector;\n  }\n\n  for (const [name, subattributeSelector] of Object.entries(attributeSelector)) {\n    attributeSelector = setWithinAttributeSelector(\n      attributeSelector,\n      name,\n      trimAttributeSelector(subattributeSelector)\n    );\n  }\n\n  if (isEmpty(attributeSelector)) {\n    return false;\n  }\n\n  return attributeSelector;\n}\n\nexport function normalizeAttributeSelector(attributeSelector: any): AttributeSelector {\n  if (attributeSelector === undefined) {\n    return false;\n  }\n\n  if (typeof attributeSelector === 'boolean') {\n    return attributeSelector;\n  }\n\n  if (isPlainObject(attributeSelector)) {\n    return attributeSelector;\n  }\n\n  throw new Error(\n    `Expected a valid attribute selector, but received a value of type '${getTypeOf(\n      attributeSelector\n    )}'`\n  );\n}\n"
  },
  {
    "path": "packages/component/src/properties/attribute.test.ts",
    "content": "import type {ExtendedError} from '@layr/utilities';\n\nimport {Component} from '../component';\nimport {EmbeddedComponent} from '../embedded-component';\nimport {Attribute} from './attribute';\nimport {isNumberValueTypeInstance} from './value-types';\nimport {sanitizers} from '../sanitization';\nimport {validators} from '../validation';\n\ndescribe('Attribute', () => {\n  test('Creation', async () => {\n    class Movie extends Component {}\n\n    const attribute = new Attribute('limit', Movie, {valueType: 'number'});\n\n    expect(Attribute.isAttribute(attribute)).toBe(true);\n    expect(attribute.getName()).toBe('limit');\n    expect(attribute.getParent()).toBe(Movie);\n    expect(isNumberValueTypeInstance(attribute.getValueType())).toBe(true);\n  });\n\n  test('Value', async () => {\n    class Movie extends Component {}\n\n    const movie = new Movie();\n\n    const attribute = new Attribute('title', movie, {valueType: 'string'});\n\n    expect(attribute.isSet()).toBe(false);\n    expect(() => attribute.getValue()).toThrow(\n      \"Cannot get the value of an unset attribute (attribute: 'Movie.prototype.title')\"\n    );\n    expect(attribute.getValue({throwIfUnset: false})).toBeUndefined();\n\n    attribute.setValue('Inception');\n\n    expect(attribute.isSet()).toBe(true);\n    expect(attribute.getValue()).toBe('Inception');\n\n    attribute.unsetValue();\n\n    expect(attribute.isSet()).toBe(false);\n\n    expect(() => attribute.setValue(123)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'Movie.prototype.title', expected type: 'string', received type: 'number')\"\n    );\n    expect(() => attribute.setValue(undefined)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'Movie.prototype.title', expected type: 'string', received type: 'undefined')\"\n    );\n  });\n\n  test('Value source', async () => {\n    class Movie extends Component {}\n\n    const movie = new Movie();\n\n    const attribute = new Attribute('title', movie, {valueType: 'string'});\n\n    attribute.setValue('Inception');\n\n    expect(attribute.getValueSource()).toBe('local');\n\n    attribute.setValue('Inception', {source: 'server'});\n\n    expect(attribute.getValueSource()).toBe('server');\n  });\n\n  test('Accessors', async () => {\n    class Movie extends Component {}\n\n    const movie = new Movie();\n\n    const attribute = new Attribute('title', movie, {\n      valueType: 'string',\n      getter() {\n        expect(this).toBe(movie);\n        return this._title;\n      },\n      setter(title) {\n        expect(this).toBe(movie);\n        this._title = title.substr(0, 1).toUpperCase() + title.substr(1);\n      }\n    });\n\n    expect(attribute.isSet()).toBe(true);\n    expect(attribute.getValue()).toBeUndefined();\n\n    attribute.setValue('inception');\n\n    expect(attribute.getValue()).toBe('Inception');\n\n    expect(\n      () =>\n        new Attribute('title', movie, {\n          setter(title) {\n            this._title = title;\n          }\n        })\n    ).toThrow(\n      \"An attribute cannot have a setter without a getter (attribute: 'Movie.prototype.title')\"\n    );\n  });\n\n  test('Initial value', async () => {\n    class Movie extends Component {}\n\n    let attribute = new Attribute('limit', Movie, {valueType: 'number'});\n\n    expect(attribute.isSet()).toBe(false);\n\n    attribute = new Attribute('limit', Movie, {valueType: 'number', value: 100});\n\n    expect(attribute.isSet()).toBe(true);\n    expect(attribute.getValue()).toBe(100);\n\n    expect(\n      () =>\n        new Attribute('limit', Movie, {\n          valueType: 'number',\n          value: 100,\n          getter() {\n            return 100;\n          }\n        })\n    ).toThrow(\n      \"An attribute cannot have both a getter or setter and an initial value (attribute: 'Movie.limit')\"\n    );\n  });\n\n  test('Default value', async () => {\n    class Movie extends Component {}\n\n    const movie = new Movie();\n\n    let attribute = new Attribute('duration', movie, {valueType: 'number?'});\n\n    expect(attribute.getDefault()).toBe(undefined);\n    expect(attribute.evaluateDefault()).toBe(undefined);\n\n    attribute = new Attribute('title', movie, {valueType: 'string', default: ''});\n\n    expect(attribute.getDefault()).toBe('');\n    expect(attribute.evaluateDefault()).toBe('');\n\n    attribute = new Attribute('title', movie, {valueType: 'string', default: () => 1 + 1});\n\n    expect(typeof attribute.getDefault()).toBe('function');\n    expect(attribute.evaluateDefault()).toBe(2);\n\n    attribute = new Attribute('movieClass', movie, {valueType: 'string', default: Movie});\n\n    expect(typeof attribute.getDefault()).toBe('function');\n    expect(attribute.evaluateDefault()).toBe(Movie);\n\n    expect(\n      () =>\n        new Attribute('title', movie, {\n          valueType: 'number?',\n          default: '',\n          getter() {\n            return '';\n          }\n        })\n    ).toThrow(\n      \"An attribute cannot have both a getter or setter and a default value (attribute: 'Movie.prototype.title')\"\n    );\n  });\n\n  test('isControlled() and markAsControlled()', async () => {\n    class Movie extends Component {}\n\n    const attribute = new Attribute('title', Movie.prototype);\n\n    expect(attribute.isControlled()).toBe(false);\n\n    attribute.setValue('Inception');\n\n    expect(attribute.getValue()).toBe('Inception');\n\n    attribute.markAsControlled();\n\n    expect(attribute.isControlled()).toBe(true);\n\n    expect(() => attribute.setValue('Inception 2')).toThrow(\n      \"Cannot set the value of a controlled attribute when the source is different than 'server' or 'store' (attribute: 'Movie.prototype.title', source: 'local')\"\n    );\n\n    expect(attribute.getValue()).toBe('Inception');\n  });\n\n  test('Sanitization', async () => {\n    class Movie extends Component {}\n\n    const movie = new Movie();\n\n    const {trim, compact} = sanitizers;\n\n    const titleAttribute = new Attribute('title', movie, {\n      valueType: 'string',\n      sanitizers: [trim()]\n    });\n\n    titleAttribute.setValue('Inception');\n\n    expect(titleAttribute.getValue()).toBe('Inception');\n\n    titleAttribute.setValue('  Inception ');\n\n    expect(titleAttribute.getValue()).toBe('Inception');\n\n    const genresAttribute = new Attribute('genres', movie, {\n      valueType: 'string[]',\n      sanitizers: [compact()],\n      items: {sanitizers: [trim()]}\n    });\n\n    genresAttribute.setValue(['drama', 'action']);\n\n    expect(genresAttribute.getValue()).toStrictEqual(['drama', 'action']);\n\n    genresAttribute.setValue(['drama ', ' action']);\n\n    expect(genresAttribute.getValue()).toStrictEqual(['drama', 'action']);\n\n    genresAttribute.setValue(['drama ', '']);\n\n    expect(genresAttribute.getValue()).toStrictEqual(['drama']);\n\n    genresAttribute.setValue(['', ' ']);\n\n    expect(genresAttribute.getValue()).toStrictEqual([]);\n  });\n\n  test('Validation', async () => {\n    class Movie extends Component {}\n\n    const movie = new Movie();\n\n    let notEmpty = validators.notEmpty();\n    let attribute = new Attribute('title', movie, {valueType: 'string?', validators: [notEmpty]});\n\n    expect(() => attribute.runValidators()).toThrow(\n      \"Cannot run the validators of an unset attribute (attribute: 'Movie.prototype.title')\"\n    );\n\n    attribute.setValue('Inception');\n\n    expect(() => attribute.validate()).not.toThrow();\n    expect(attribute.isValid()).toBe(true);\n    expect(attribute.runValidators()).toEqual([]);\n\n    attribute.setValue('');\n\n    expect(() => attribute.validate()).toThrow(\n      \"The following error(s) occurred while validating the attribute 'title': The validator `notEmpty()` failed (path: '')\"\n    );\n    expect(attribute.isValid()).toBe(false);\n    expect(attribute.runValidators()).toEqual([{validator: notEmpty, path: ''}]);\n\n    attribute.setValue(undefined);\n\n    expect(() => attribute.validate()).toThrow(\n      \"The following error(s) occurred while validating the attribute 'title': The validator `notEmpty()` failed (path: '')\"\n    );\n    expect(attribute.isValid()).toBe(false);\n    expect(attribute.runValidators()).toEqual([{validator: notEmpty, path: ''}]);\n\n    // --- With a custom message ---\n\n    notEmpty = validators.notEmpty('The title cannot be empty.');\n    attribute = new Attribute('title', movie, {valueType: 'string?', validators: [notEmpty]});\n\n    attribute.setValue('Inception');\n\n    expect(() => attribute.validate()).not.toThrow();\n\n    attribute.setValue('');\n\n    let error: ExtendedError;\n\n    try {\n      attribute.validate();\n    } catch (err: any) {\n      error = err;\n    }\n\n    expect(error!.message).toBe(\n      \"The following error(s) occurred while validating the attribute 'title': The title cannot be empty. (path: '')\"\n    );\n\n    expect(error!.displayMessage).toBe('The title cannot be empty.');\n  });\n\n  test('Observability', async () => {\n    class Movie extends Component {}\n\n    const movie = new Movie();\n\n    const movieObserver = jest.fn();\n    movie.addObserver(movieObserver);\n\n    const titleAttribute = new Attribute('title', movie, {valueType: 'string'});\n\n    const titleObserver = jest.fn();\n    titleAttribute.addObserver(titleObserver);\n\n    expect(titleObserver).toHaveBeenCalledTimes(0);\n    expect(movieObserver).toHaveBeenCalledTimes(0);\n\n    titleAttribute.setValue('Inception');\n\n    expect(titleObserver).toHaveBeenCalledTimes(1);\n    expect(movieObserver).toHaveBeenCalledTimes(1);\n\n    titleAttribute.setValue('Inception 2');\n\n    expect(titleObserver).toHaveBeenCalledTimes(2);\n    expect(movieObserver).toHaveBeenCalledTimes(2);\n\n    titleAttribute.setValue('Inception 2');\n\n    // Assigning the same value should not call the observers\n    expect(titleObserver).toHaveBeenCalledTimes(2);\n    expect(movieObserver).toHaveBeenCalledTimes(2);\n\n    const tagsAttribute = new Attribute('title', movie, {valueType: 'string[]'});\n\n    const tagsObserver = jest.fn();\n    tagsAttribute.addObserver(tagsObserver);\n\n    expect(tagsObserver).toHaveBeenCalledTimes(0);\n    expect(movieObserver).toHaveBeenCalledTimes(2);\n\n    tagsAttribute.setValue(['drama', 'action']);\n\n    expect(tagsObserver).toHaveBeenCalledTimes(1);\n    expect(movieObserver).toHaveBeenCalledTimes(3);\n\n    const tagArray = tagsAttribute.getValue() as string[];\n\n    tagArray[0] = 'Drama';\n\n    expect(tagsObserver).toHaveBeenCalledTimes(2);\n    expect(movieObserver).toHaveBeenCalledTimes(4);\n\n    tagArray[0] = 'Drama';\n\n    // Assigning the same value should not call the observers\n    expect(tagsObserver).toHaveBeenCalledTimes(2);\n    expect(movieObserver).toHaveBeenCalledTimes(4);\n\n    tagsAttribute.setValue(['Drama', 'Action']);\n\n    expect(tagsObserver).toHaveBeenCalledTimes(3);\n    expect(movieObserver).toHaveBeenCalledTimes(5);\n\n    const newTagArray = tagsAttribute.getValue() as string[];\n\n    newTagArray[0] = 'drama';\n\n    expect(tagsObserver).toHaveBeenCalledTimes(4);\n    expect(movieObserver).toHaveBeenCalledTimes(6);\n\n    tagArray[0] = 'DRAMA';\n\n    // Modifying the previous array should not call the observers\n    expect(tagsObserver).toHaveBeenCalledTimes(4);\n    expect(movieObserver).toHaveBeenCalledTimes(6);\n\n    tagsAttribute.unsetValue();\n\n    expect(tagsObserver).toHaveBeenCalledTimes(5);\n    expect(movieObserver).toHaveBeenCalledTimes(7);\n\n    tagsAttribute.unsetValue();\n\n    // Calling unset again should not call the observers\n    expect(tagsObserver).toHaveBeenCalledTimes(5);\n    expect(movieObserver).toHaveBeenCalledTimes(7);\n\n    newTagArray[0] = 'drama';\n\n    // Modifying the detached array should not call the observers\n    expect(tagsObserver).toHaveBeenCalledTimes(5);\n    expect(movieObserver).toHaveBeenCalledTimes(7);\n\n    // --- With an embedded component ---\n\n    class UserDetails extends EmbeddedComponent {}\n\n    const userDetails = new UserDetails();\n\n    const countryAttribute = new Attribute('country', userDetails, {valueType: 'string'});\n\n    class User extends Component {}\n\n    User.provideComponent(UserDetails);\n\n    let user = new User();\n\n    let userObserver = jest.fn();\n    user.addObserver(userObserver);\n\n    const detailsAttribute = new Attribute('details', user, {valueType: 'UserDetails?'});\n\n    expect(userObserver).toHaveBeenCalledTimes(0);\n\n    detailsAttribute.setValue(userDetails);\n\n    expect(userObserver).toHaveBeenCalledTimes(1);\n\n    countryAttribute.setValue('Japan');\n\n    expect(userObserver).toHaveBeenCalledTimes(2);\n\n    detailsAttribute.setValue(undefined);\n\n    expect(userObserver).toHaveBeenCalledTimes(3);\n\n    countryAttribute.setValue('France');\n\n    expect(userObserver).toHaveBeenCalledTimes(3);\n\n    // --- With an array of embedded components ---\n\n    class Organization extends EmbeddedComponent {}\n\n    const organization = new Organization();\n\n    const organizationNameAttribute = new Attribute('name', organization, {valueType: 'string'});\n\n    User.provideComponent(Organization);\n\n    user = new User();\n\n    userObserver = jest.fn();\n    user.addObserver(userObserver);\n\n    const organizationsAttribute = new Attribute('organizations', user, {\n      valueType: 'Organization[]'\n    });\n\n    expect(userObserver).toHaveBeenCalledTimes(0);\n\n    organizationsAttribute.setValue([]);\n\n    expect(userObserver).toHaveBeenCalledTimes(1);\n\n    (organizationsAttribute.getValue() as Organization[]).push(organization);\n\n    expect(userObserver).toHaveBeenCalledTimes(2);\n\n    organizationNameAttribute.setValue('The Inc.');\n\n    expect(userObserver).toHaveBeenCalledTimes(3);\n\n    (organizationsAttribute.getValue() as Organization[]).pop();\n\n    expect(userObserver).toHaveBeenCalledTimes(5);\n\n    organizationNameAttribute.setValue('Nice Inc.');\n\n    expect(userObserver).toHaveBeenCalledTimes(5);\n\n    // --- With a referenced component ---\n\n    class Blog extends Component {}\n\n    const blog = new Blog();\n\n    const blogNameAttribute = new Attribute('name', blog, {valueType: 'string'});\n\n    class Article extends Component {}\n\n    Article.provideComponent(Blog);\n\n    let article = new Article();\n\n    let articleObserver = jest.fn();\n    article.addObserver(articleObserver);\n\n    const blogAttribute = new Attribute('blog', article, {valueType: 'Blog?'});\n\n    expect(articleObserver).toHaveBeenCalledTimes(0);\n\n    blogAttribute.setValue(blog);\n\n    expect(articleObserver).toHaveBeenCalledTimes(1);\n\n    blogNameAttribute.setValue('My Blog');\n\n    expect(articleObserver).toHaveBeenCalledTimes(1);\n\n    blogAttribute.setValue(undefined);\n\n    expect(articleObserver).toHaveBeenCalledTimes(2);\n\n    blogNameAttribute.setValue('The Blog');\n\n    expect(articleObserver).toHaveBeenCalledTimes(2);\n\n    // --- With an array of referenced components ---\n\n    class Comment extends Component {}\n\n    const comment = new Comment();\n\n    const commentTextAttribute = new Attribute('text', comment, {valueType: 'string'});\n\n    Article.provideComponent(Comment);\n\n    article = new Article();\n\n    articleObserver = jest.fn();\n    article.addObserver(articleObserver);\n\n    const commentsAttribute = new Attribute('comments', article, {valueType: 'Comment[]'});\n\n    expect(articleObserver).toHaveBeenCalledTimes(0);\n\n    commentsAttribute.setValue([]);\n\n    expect(articleObserver).toHaveBeenCalledTimes(1);\n\n    (commentsAttribute.getValue() as Comment[]).push(comment);\n\n    expect(articleObserver).toHaveBeenCalledTimes(2);\n\n    commentTextAttribute.setValue('Hello');\n\n    expect(articleObserver).toHaveBeenCalledTimes(2);\n\n    (commentsAttribute.getValue() as Comment[]).pop();\n\n    expect(articleObserver).toHaveBeenCalledTimes(4);\n\n    commentTextAttribute.setValue('Hey');\n\n    expect(articleObserver).toHaveBeenCalledTimes(4);\n  });\n\n  test('Forking', async () => {\n    class Movie extends Component {}\n\n    const movie = new Movie();\n\n    const attribute = new Attribute('title', movie, {valueType: 'string'});\n    attribute.setValue('Inception');\n\n    expect(attribute.getValue()).toBe('Inception');\n\n    const movieFork = Object.create(movie);\n    const attributeFork = attribute.fork(movieFork);\n\n    expect(attributeFork.getValue()).toBe('Inception');\n\n    attributeFork.setValue('Inception 2');\n\n    expect(attributeFork.getValue()).toBe('Inception 2');\n    expect(attribute.getValue()).toBe('Inception');\n  });\n\n  test('Introspection', async () => {\n    class Movie extends Component {}\n\n    expect(\n      new Attribute('limit', Movie, {valueType: 'number', exposure: {get: true}}).introspect()\n    ).toStrictEqual({\n      name: 'limit',\n      type: 'Attribute',\n      exposure: {get: true},\n      valueType: 'number'\n    });\n\n    expect(\n      new Attribute('limit', Movie, {\n        valueType: 'number',\n        value: 100,\n        exposure: {set: true}\n      }).introspect()\n    ).toStrictEqual({name: 'limit', type: 'Attribute', exposure: {set: true}, valueType: 'number'});\n\n    expect(\n      new Attribute('limit', Movie, {\n        valueType: 'number',\n        value: 100,\n        exposure: {get: true}\n      }).introspect()\n    ).toStrictEqual({\n      name: 'limit',\n      type: 'Attribute',\n      value: 100,\n      exposure: {get: true},\n      valueType: 'number'\n    });\n\n    const defaultTitle = function () {\n      return '';\n    };\n\n    expect(\n      new Attribute('title', Movie.prototype, {\n        valueType: 'string',\n        default: defaultTitle,\n        exposure: {get: true, set: true}\n      }).introspect()\n    ).toStrictEqual({\n      name: 'title',\n      type: 'Attribute',\n      default: defaultTitle,\n      exposure: {get: true, set: true},\n      valueType: 'string'\n    });\n\n    // When 'set' is not exposed, the default value should not be exposed\n    expect(\n      new Attribute('title', Movie.prototype, {\n        valueType: 'string',\n        default: defaultTitle,\n        exposure: {get: true}\n      }).introspect()\n    ).toStrictEqual({\n      name: 'title',\n      type: 'Attribute',\n      exposure: {get: true},\n      valueType: 'string'\n    });\n\n    const notEmpty = validators.notEmpty();\n\n    expect(\n      new Attribute('title', Movie.prototype, {\n        valueType: 'string?',\n        validators: [notEmpty],\n        exposure: {get: true}\n      }).introspect()\n    ).toStrictEqual({\n      name: 'title',\n      type: 'Attribute',\n      valueType: 'string?',\n      exposure: {get: true},\n      validators: [notEmpty]\n    });\n\n    expect(\n      new Attribute('tags', Movie.prototype, {\n        valueType: 'string[]',\n        exposure: {get: true}\n      }).introspect()\n    ).toStrictEqual({\n      name: 'tags',\n      type: 'Attribute',\n      valueType: 'string[]',\n      exposure: {get: true}\n    });\n\n    expect(\n      new Attribute('tags', Movie.prototype, {\n        valueType: 'string[]',\n        items: {validators: [notEmpty]},\n        exposure: {get: true}\n      }).introspect()\n    ).toStrictEqual({\n      name: 'tags',\n      type: 'Attribute',\n      valueType: 'string[]',\n      items: {\n        validators: [notEmpty]\n      },\n      exposure: {get: true}\n    });\n\n    expect(\n      new Attribute('tags', Movie.prototype, {\n        valueType: 'string[][]',\n        items: {items: {validators: [notEmpty]}},\n        exposure: {get: true}\n      }).introspect()\n    ).toStrictEqual({\n      name: 'tags',\n      type: 'Attribute',\n      valueType: 'string[][]',\n      items: {\n        items: {\n          validators: [notEmpty]\n        }\n      },\n      exposure: {get: true}\n    });\n  });\n\n  test('Unintrospection', async () => {\n    class Movie extends Component {}\n\n    let {name, options} = Attribute.unintrospect({\n      name: 'limit',\n      type: 'Attribute',\n      valueType: 'number',\n      exposure: {get: true}\n    });\n\n    expect({name, options}).toEqual({\n      name: 'limit',\n      options: {valueType: 'number', exposure: {get: true}}\n    });\n    expect(() => new Attribute(name, Movie, options)).not.toThrow();\n\n    ({name, options} = Attribute.unintrospect({\n      name: 'limit',\n      type: 'Attribute',\n      valueType: 'number',\n      value: 100,\n      exposure: {get: true}\n    }));\n\n    expect({name, options}).toEqual({\n      name: 'limit',\n      options: {valueType: 'number', value: 100, exposure: {get: true}}\n    });\n    expect(() => new Attribute(name, Movie, options)).not.toThrow();\n\n    const defaultTitle = function () {\n      return '';\n    };\n\n    ({name, options} = Attribute.unintrospect({\n      name: 'title',\n      type: 'Attribute',\n      valueType: 'string',\n      default: defaultTitle,\n      exposure: {get: true, set: true}\n    }));\n\n    expect({name, options}).toEqual({\n      name: 'title',\n      options: {valueType: 'string', default: defaultTitle, exposure: {get: true, set: true}}\n    });\n    expect(() => new Attribute(name, Movie.prototype, options)).not.toThrow();\n\n    const notEmptyValidator = validators.notEmpty();\n\n    ({name, options} = Attribute.unintrospect({\n      name: 'title',\n      type: 'Attribute',\n      valueType: 'string?',\n      validators: [notEmptyValidator],\n      exposure: {get: true}\n    }));\n\n    expect(name).toBe('title');\n    expect(options.valueType).toBe('string?');\n    expect(options.validators).toEqual([notEmptyValidator]);\n    expect(options.exposure).toEqual({get: true});\n    expect(() => new Attribute(name, Movie.prototype, options)).not.toThrow();\n\n    ({name, options} = Attribute.unintrospect({\n      name: 'tags',\n      type: 'Attribute',\n      valueType: 'string[]',\n      items: {\n        validators: [notEmptyValidator]\n      },\n      exposure: {get: true}\n    }));\n\n    expect(name).toBe('tags');\n    expect(options.valueType).toBe('string[]');\n    expect(options.validators).toBeUndefined();\n    expect(options.items!.validators).toEqual([notEmptyValidator]);\n    expect(options.exposure).toEqual({get: true});\n    expect(() => new Attribute(name, Movie.prototype, options)).not.toThrow();\n\n    ({name, options} = Attribute.unintrospect({\n      name: 'tags',\n      type: 'Attribute',\n      valueType: 'string[][]',\n      items: {\n        items: {\n          validators: [notEmptyValidator]\n        }\n      },\n      exposure: {get: true}\n    }));\n\n    expect(name).toBe('tags');\n    expect(options.valueType).toBe('string[][]');\n    expect(options.validators).toBeUndefined();\n    expect(options.items!.validators).toBeUndefined();\n    expect(options.items!.items!.validators).toEqual([notEmptyValidator]);\n    expect(options.exposure).toEqual({get: true});\n    expect(() => new Attribute(name, Movie.prototype, options)).not.toThrow();\n  });\n});\n"
  },
  {
    "path": "packages/component/src/properties/attribute.ts",
    "content": "import {hasOwnProperty} from 'core-helpers';\nimport {\n  Observable,\n  createObservable,\n  isObservable,\n  canBeObserved,\n  isEmbeddable,\n  ObserverPayload\n} from '@layr/observable';\nimport {throwError} from '@layr/utilities';\nimport {possiblyAsync} from 'possibly-async';\n\nimport type {\n  Component,\n  TraverseAttributesIteratee,\n  TraverseAttributesOptions,\n  ResolveAttributeSelectorOptions\n} from '../component';\nimport {Property, PropertyOptions, IntrospectedProperty, UnintrospectedProperty} from './property';\nimport {\n  ValueType,\n  IntrospectedValueType,\n  UnintrospectedValueType,\n  createValueType,\n  unintrospectValueType\n} from './value-types';\nimport {fork} from '../forking';\nimport {AttributeSelector} from './attribute-selector';\nimport type {Sanitizer, SanitizerFunction} from '../sanitization';\nimport type {Validator, ValidatorFunction} from '../validation';\nimport {SerializeOptions} from '../serialization';\nimport {deserialize, DeserializeOptions} from '../deserialization';\nimport {isComponentClass, isComponentInstance, ensureComponentClass} from '../utilities';\n\nexport type AttributeOptions = PropertyOptions & {\n  valueType?: string;\n  value?: unknown;\n  default?: unknown;\n  sanitizers?: (Sanitizer | SanitizerFunction)[];\n  validators?: (Validator | ValidatorFunction)[];\n  items?: AttributeItemsOptions;\n  getter?: (this: any) => unknown;\n  setter?: (this: any, value: any) => void;\n};\n\ntype AttributeItemsOptions = {\n  sanitizers?: (Sanitizer | SanitizerFunction)[];\n  validators?: (Validator | ValidatorFunction)[];\n  items?: AttributeItemsOptions;\n};\n\nexport type ValueSource = 'server' | 'store' | 'local' | 'client';\n\nexport type IntrospectedAttribute = IntrospectedProperty & {\n  value?: unknown;\n  default?: unknown;\n} & IntrospectedValueType;\n\nexport type UnintrospectedAttribute = UnintrospectedProperty & {\n  options: {\n    value?: unknown;\n    default?: unknown;\n  } & UnintrospectedValueType;\n};\n\n/**\n * *Inherits from [`Property`](https://layrjs.com/docs/v2/reference/property) and [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class).*\n *\n * An `Attribute` represents an attribute of a [Component](https://layrjs.com/docs/v2/reference/component) class, prototype, or instance. It plays the role of a regular JavaScript object attribute, but brings some extra features such as runtime type checking, validation, serialization, or observability.\n *\n * #### Usage\n *\n * Typically, you create an `Attribute` and associate it to a component by using the [`@attribute()`](https://layrjs.com/docs/v2/reference/component#attribute-decorator) decorator.\n *\n * For example, here is how you would define a `Movie` class with some attributes:\n *\n * ```\n * // JS\n *\n * import {Component, attribute, validators} from '﹫layr/component';\n *\n * const {minLength} = validators;\n *\n * class Movie extends Component {\n *   // Optional 'string' class attribute\n *   ﹫attribute('string?') static customName;\n *\n *   // Required 'string' instance attribute\n *   ﹫attribute('string') title;\n *\n *   // Optional 'string' instance attribute with a validator and a default value\n *   ﹫attribute('string?', {validators: [minLength(16)]}) summary = '';\n * }\n * ```\n *\n * ```\n * // TS\n *\n * import {Component, attribute, validators} from '﹫layr/component';\n *\n * const {minLength} = validators;\n *\n * class Movie extends Component {\n *   // Optional 'string' class attribute\n *   ﹫attribute('string?') static customName?: string;\n *\n *   // Required 'string' instance attribute\n *   ﹫attribute('string') title!: string;\n *\n *   // Optional 'string' instance attribute with a validator and a default value\n *   ﹫attribute('string?', {validators: [minLength(16)]}) summary? = '';\n * }\n * ```\n *\n * Then you can access the attributes like you would normally do with regular JavaScript objects:\n *\n * ```\n * Movie.customName = 'Film';\n * Movie.customName; // => 'Film'\n *\n * const movie = new Movie({title: 'Inception'});\n * movie.title; // => 'Inception'\n * movie.title = 'Inception 2';\n * movie.title; // => 'Inception 2'\n * movie.summary; // => '' (default value)\n * ```\n *\n * And you can take profit of some extra features:\n *\n * ```\n * // Runtime type checking\n * movie.title = 123; // Error\n * movie.title = undefined; // Error\n *\n * // Validation\n * movie.summary = undefined;\n * movie.isValid(); // => true (movie.summary is optional)\n * movie.summary = 'A nice movie.';\n * movie.isValid(); // => false (movie.summary is too short)\n * movie.summary = 'An awesome movie.'\n * movie.isValid(); // => true\n *\n * // Serialization\n * movie.serialize();\n * // => {__component: 'Movie', title: 'Inception 2', summary: 'An awesome movie.'}\n * ```\n */\nexport class Attribute extends Observable(Property) {\n  /**\n   * Creates an instance of [`Attribute`](https://layrjs.com/docs/v2/reference/attribute). Typically, instead of using this constructor, you would rather use the [`@attribute()`](https://layrjs.com/docs/v2/reference/component#attribute-decorator) decorator.\n   *\n   * @param name The name of the attribute.\n   * @param parent The component class, prototype, or instance that owns the attribute.\n   * @param [options.valueType] A string specifying the [type of values](https://layrjs.com/docs/v2/reference/value-type#supported-types) the attribute can store (default: `'any'`).\n   * @param [options.value] The initial value of the attribute (usable for class attributes only).\n   * @param [options.default] The default value (or a function returning the default value) of the attribute (usable for instance attributes only).\n   * @param [options.sanitizers] An array of [sanitizers](https://layrjs.com/docs/v2/reference/sanitizer) for the value of the attribute.\n   * @param [options.validators] An array of [validators](https://layrjs.com/docs/v2/reference/validator) for the value of the attribute.\n   * @param [options.items.sanitizers] An array of [sanitizers](https://layrjs.com/docs/v2/reference/sanitizer) for the items of an array attribute.\n   * @param [options.items.validators] An array of [validators](https://layrjs.com/docs/v2/reference/validator) for the items of an array attribute.\n   * @param [options.getter] A getter function for getting the value of the attribute. Plays the same role as a regular [JavaScript getter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get).\n   * @param [options.setter] A setter function for setting the value of the attribute. Plays the same role as a regular [JavaScript setter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set).\n   * @param [options.exposure] A [`PropertyExposure`](https://layrjs.com/docs/v2/reference/property#property-exposure-type) object specifying how the attribute should be exposed to remote access.\n   *\n   * @returns The [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) instance that was created.\n   *\n   * @example\n   * ```\n   * import {Component, Attribute} from '﹫layr/component';\n   *\n   * class Movie extends Component {}\n   *\n   * const title = new Attribute('title', Movie.prototype, {valueType: 'string'});\n   *\n   * title.getName(); // => 'title'\n   * title.getParent(); // => Movie.prototype\n   * title.getValueType().toString(); // => 'string'\n   * ```\n   *\n   * @category Creation\n   */\n  constructor(name: string, parent: typeof Component | Component, options: AttributeOptions = {}) {\n    super(name, parent, options);\n  }\n\n  _initialize() {\n    this.addObserver(this._onChange.bind(this));\n  }\n\n  // === Options ===\n\n  _getter?: () => unknown;\n  _setter?: (value: any) => void;\n\n  setOptions(options: AttributeOptions = {}) {\n    const {\n      valueType,\n      value: initialValue,\n      default: defaultValue,\n      sanitizers,\n      validators,\n      items,\n      getter,\n      setter,\n      ...otherOptions\n    } = options;\n\n    const hasInitialValue = 'value' in options;\n    const hasDefaultValue = 'default' in options;\n\n    super.setOptions(otherOptions);\n\n    this._valueType = createValueType(valueType, this, {sanitizers, validators, items});\n\n    if (getter !== undefined || setter !== undefined) {\n      if (initialValue !== undefined) {\n        throw new Error(\n          `An attribute cannot have both a getter or setter and an initial value (${this.describe()})`\n        );\n      }\n\n      if (defaultValue !== undefined) {\n        throw new Error(\n          `An attribute cannot have both a getter or setter and a default value (${this.describe()})`\n        );\n      }\n\n      if (getter !== undefined) {\n        this._getter = getter;\n      }\n\n      if (setter !== undefined) {\n        if (getter === undefined) {\n          throw new Error(\n            `An attribute cannot have a setter without a getter (${this.describe()})`\n          );\n        }\n        this._setter = setter;\n      }\n\n      this._isSet = true;\n\n      return;\n    }\n\n    if (hasInitialValue) {\n      this.setValue(initialValue);\n    }\n\n    if (hasDefaultValue) {\n      this._default = defaultValue;\n    }\n  }\n\n  // === Property Methods ===\n\n  /**\n   * See the methods that are inherited from the [`Property`](https://layrjs.com/docs/v2/reference/property#basic-methods) class.\n   *\n   * @category Property Methods\n   */\n\n  // === Value type ===\n\n  _valueType!: ValueType;\n\n  /**\n   * Returns the type of values the attribute can store.\n   *\n   * @returns A [ValueType](https://layrjs.com/docs/v2/reference/value-type) instance.\n   *\n   * @example\n   * ```\n   * const title = Movie.prototype.getAttribute('title');\n   * title.getValueType(); // => A ValueType instance\n   * title.getValueType().toString(); // => 'string'\n   * title.getValueType().isOptional(); // => false\n   * ```\n   *\n   * @category Value Type\n   */\n  getValueType() {\n    return this._valueType;\n  }\n\n  // === Value ===\n\n  _value?: unknown;\n  _isSet?: boolean;\n\n  /**\n   * Returns the current value of the attribute.\n   *\n   * @param [options.throwIfUnset] A boolean specifying whether the method should throw an error if the value is not set (default: `true`). If `false` is specified and the value is not set, the method returns `undefined`.\n   *\n   * @returns A value of the type handled by the attribute.\n   *\n   * @example\n   * ```\n   * const title = movie.getAttribute('title');\n   * title.getValue(); // => 'Inception'\n   * title.unsetValue();\n   * title.getValue(); // => Error\n   * title.getValue({throwIfUnset: false}); // => undefined\n   * ```\n   *\n   * @category Value\n   */\n  getValue(options: {throwIfUnset?: boolean; autoFork?: boolean} = {}) {\n    const {throwIfUnset = true, autoFork = true} = options;\n\n    if (!this.isSet()) {\n      if (throwIfUnset) {\n        throw new Error(`Cannot get the value of an unset attribute (${this.describe()})`);\n      }\n      return undefined;\n    }\n\n    if (this._getter !== undefined) {\n      return this._getter.call(this.getParent());\n    }\n\n    if (autoFork && !hasOwnProperty(this, '_value')) {\n      const parent = this.getParent();\n      const value = this._value;\n      const componentClass = isComponentInstance(value)\n        ? ensureComponentClass(parent).getComponent(value.constructor.getComponentName())\n        : undefined;\n\n      let valueFork = fork(value, {componentClass});\n\n      if (canBeObserved(valueFork)) {\n        if (!isObservable(valueFork)) {\n          valueFork = createObservable(valueFork);\n        }\n\n        if (isEmbeddable(valueFork)) {\n          valueFork.addObserver(this);\n        }\n      }\n\n      this._value = valueFork;\n    }\n\n    return this._value;\n  }\n\n  _ignoreNextSetValueCall?: boolean;\n\n  /**\n   * Sets the value of the attribute. If the type of the value doesn't match the expected type, an error is thrown.\n   *\n   * When the attribute's value changes, the observers of the attribute are automatically executed, and the observers of the parent component are executed as well.\n   *\n   * @param value The value to be set.\n   * @param [options.source] A string specifying the [source of the value](https://layrjs.com/docs/v2/reference/attribute#value-source-type) (default: `'local'`).\n   *\n   * @example\n   * ```\n   * const title = movie.getAttribute('title');\n   * title.setValue('Inception 2');\n   * title.setValue(123); // => Error\n   * ```\n   *\n   * @category Value\n   */\n  setValue(value: unknown, {source = 'local'}: {source?: ValueSource} = {}) {\n    if (hasOwnProperty(this, '_ignoreNextSetValueCall')) {\n      delete this._ignoreNextSetValueCall;\n      return {previousValue: undefined, newValue: undefined};\n    }\n\n    if (this.isControlled() && !(source === 'server' || source === 'store')) {\n      throw new Error(\n        `Cannot set the value of a controlled attribute when the source is different than 'server' or 'store' (${this.describe()}, source: '${source}')`\n      );\n    }\n\n    this.checkValue(value);\n\n    value = this.sanitizeValue(value);\n\n    if (this._setter !== undefined) {\n      this._setter.call(this.getParent(), value);\n      return {previousValue: undefined, newValue: undefined};\n    }\n\n    if (this._getter !== undefined) {\n      throw new Error(\n        `Cannot set the value of an attribute that has a getter but no setter (${this.describe()})`\n      );\n    }\n\n    if (canBeObserved(value) && !isObservable(value)) {\n      value = createObservable(value);\n    }\n\n    const previousValue = this.getValue({throwIfUnset: false});\n    this._value = value;\n    this._isSet = true;\n\n    const valueHasChanged = (value as any)?.valueOf() !== (previousValue as any)?.valueOf();\n\n    if (valueHasChanged) {\n      if (isObservable(previousValue) && isEmbeddable(previousValue)) {\n        previousValue.removeObserver(this);\n      }\n\n      if (isObservable(value) && isEmbeddable(value)) {\n        value.addObserver(this);\n      }\n    }\n\n    if (valueHasChanged || source !== this._source) {\n      this.callObservers({source});\n    }\n\n    return {previousValue, newValue: value};\n  }\n\n  /**\n   * Unsets the value of the attribute. If the value is already unset, nothing happens.\n   *\n   * @example\n   * ```\n   * const title = movie.getAttribute('title');\n   * title.isSet(); // => true\n   * title.unsetValue();\n   * title.isSet(); // => false\n   * ```\n   *\n   * @category Value\n   */\n  unsetValue() {\n    if (this._getter !== undefined) {\n      throw new Error(\n        `Cannot unset the value of an attribute that has a getter (${this.describe()})`\n      );\n    }\n\n    if (this._isSet !== true) {\n      return {previousValue: undefined};\n    }\n\n    const previousValue = this.getValue({throwIfUnset: false});\n    this._value = undefined;\n    this._isSet = false;\n\n    if (isObservable(previousValue) && isEmbeddable(previousValue)) {\n      previousValue.removeObserver(this);\n    }\n\n    this.callObservers({source: 'local'});\n\n    return {previousValue};\n  }\n\n  /**\n   * Returns whether the value of the attribute is set or not.\n   *\n   * @returns A boolean.\n   *\n   * @example\n   * ```\n   * const title = movie.getAttribute('title');\n   * title.isSet(); // => true\n   * title.unsetValue();\n   * title.isSet(); // => false\n   * ```\n   *\n   * @category Value\n   */\n  isSet() {\n    return this._isSet === true;\n  }\n\n  checkValue(value: unknown) {\n    return this.getValueType().checkValue(value, this);\n  }\n\n  sanitizeValue(value: unknown) {\n    return this.getValueType().sanitizeValue(value);\n  }\n\n  // === Value source ===\n\n  _source: ValueSource = 'local';\n\n  /**\n   * Returns the source of the value of the attribute.\n   *\n   * @returns A [`ValueSource`](https://layrjs.com/docs/v2/reference/attribute#value-source-type) string.\n   *\n   * @example\n   * ```\n   * const title = movie.getAttribute('title');\n   * title.getValueSource(); // => 'local' (the value was set locally)\n   * ```\n   *\n   * @category Value Source\n   */\n  getValueSource() {\n    return this._source;\n  }\n\n  /**\n   * Sets the source of the value of the attribute.\n   *\n   * @param source A [`ValueSource`](https://layrjs.com/docs/v2/reference/attribute#value-source-type) string.\n   *\n   * @example\n   * ```\n   * const title = movie.getAttribute('title');\n   * title.setValueSource('local'); // The value was set locally\n   * title.setValueSource('server'); // The value came from an upper layer\n   * title.setValueSource('client'); // The value came from a lower layer\n   * ```\n   *\n   * @category Value Source\n   */\n  setValueSource(source: ValueSource) {\n    if (source !== this._source) {\n      this._source = source;\n      this.callObservers({source});\n    }\n  }\n\n  /**\n   * @typedef ValueSource\n   *\n   * A string representing the source of a value.\n   *\n   * Currently, four types of sources are supported:\n   *\n   * * `'server'`: The value comes from an upper layer.\n   * * `'store'`: The value comes from a store.\n   * * `'local'`: The value comes from the current layer.\n   * * `'client'`: The value comes from a lower layer.\n   *\n   * @category Value Source\n   */\n\n  // === Default value ===\n\n  _default?: unknown;\n\n  /**\n   * Returns the default value of the attribute as specified when the attribute was created.\n   *\n   * @returns A value or a function returning a value.\n   *\n   * @example\n   * ```\n   * const summary = movie.getAttribute('summary');\n   * summary.getDefault(); // => function () { return ''; }\n   * ```\n   *\n   * @category Default Value\n   */\n  getDefault() {\n    return this._default;\n  }\n\n  /**\n   * Evaluate the default value of the attribute. If the default value is a function, the function is called (with the attribute's parent as `this` context), and the result is returned. Otherwise, the default value is returned as is.\n   *\n   * @returns A value of any type.\n   *\n   * @example\n   * ```\n   * const summary = movie.getAttribute('summary');\n   * summary.evaluateDefault(); // ''\n   * ```\n   *\n   * @category Default Value\n   */\n  evaluateDefault() {\n    let value = this._default;\n\n    if (typeof value === 'function' && !isComponentClass(value)) {\n      value = value.call(this.getParent());\n    }\n\n    return value;\n  }\n\n  _isDefaultSetInConstructor?: boolean;\n\n  _fixDecoration() {\n    if (this._isDefaultSetInConstructor) {\n      this._ignoreNextSetValueCall = true;\n    }\n  }\n\n  // === 'isControlled' mark\n\n  _isControlled?: boolean;\n\n  isControlled() {\n    return this._isControlled === true;\n  }\n\n  markAsControlled() {\n    Object.defineProperty(this, '_isControlled', {value: true});\n  }\n\n  // === Observers ===\n\n  _onChange(payload: ObserverPayload & {source?: ValueSource}) {\n    const {source = 'local'} = payload;\n\n    if (source !== this._source) {\n      this._source = source;\n    }\n\n    this.getParent().callObservers(payload);\n  }\n\n  // === Attribute traversal ===\n\n  _traverseAttributes(iteratee: TraverseAttributesIteratee, options: TraverseAttributesOptions) {\n    const {setAttributesOnly} = options;\n\n    const value = setAttributesOnly ? this.getValue() : undefined;\n\n    this.getValueType()._traverseAttributes(iteratee, this, value, options);\n  }\n\n  // === Attribute selectors ===\n\n  _resolveAttributeSelector(\n    normalizedAttributeSelector: AttributeSelector,\n    options: ResolveAttributeSelectorOptions\n  ) {\n    const {setAttributesOnly} = options;\n\n    const value = setAttributesOnly ? this.getValue() : undefined;\n\n    return this.getValueType()._resolveAttributeSelector(\n      normalizedAttributeSelector,\n      this,\n      value,\n      options\n    );\n  }\n\n  // === Serialization ===\n\n  serialize(options: SerializeOptions = {}): unknown {\n    if (!this.isSet()) {\n      throw new Error(`Cannot serialize an unset attribute (${this.describe()})`);\n    }\n\n    return this.getValueType().serializeValue(this.getValue(), this, options);\n  }\n\n  // === Deserialization ===\n\n  deserialize(\n    serializedValue: unknown,\n    options: DeserializeOptions = {}\n  ): void | PromiseLike<void> {\n    if (this.isSet()) {\n      const value = this.getValue();\n\n      if (value !== undefined && this.getValueType().canDeserializeInPlace(this)) {\n        return (value as any).deserialize(serializedValue, options);\n      }\n    }\n\n    const rootComponent = ensureComponentClass(this.getParent());\n\n    return possiblyAsync(\n      deserialize(serializedValue, {...options, rootComponent}),\n      (deserializedValue) => {\n        this.setValue(deserializedValue, {source: options.source});\n      }\n    );\n  }\n\n  // === Validation ===\n\n  /**\n   * Validates the value of the attribute. If the value doesn't pass the validation, an error is thrown. The error is a JavaScript `Error` instance with a `failedValidators` custom attribute which contains the result of the [`runValidators()`](https://layrjs.com/docs/v2/reference/attribute#run-validators-instance-method) method.\n   *\n   * @param [attributeSelector] In case the value of the attribute is a component, your can pass an [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the component's attributes to be validated (default: `true`, which means that all the component's attributes will be validated).\n   *\n   * @example\n   * ```\n   * // JS\n   *\n   * import {Component, attribute, validators} from '﹫layr/component';\n   *\n   * const {notEmpty} = validators;\n   *\n   * class Movie extends Component {\n   *   ﹫attribute('string', {validators: [notEmpty()]}) title;\n   * }\n   *\n   * const movie = new Movie({title: 'Inception'});\n   * const title = movie.getAttribute('title');\n   *\n   * title.getValue(); // => 'Inception'\n   * title.validate(); // All good!\n   * title.setValue('');\n   * title.validate(); // => Error {failedValidators: [{validator: ..., path: ''}]}\n   * ```\n   *\n   * @example\n   * ```\n   * // TS\n   *\n   * import {Component, attribute, validators} from '﹫layr/component';\n   *\n   * const {notEmpty} = validators;\n   *\n   * class Movie extends Component {\n   *   ﹫attribute('string', {validators: [notEmpty()]}) title!: string;\n   * }\n   *\n   * const movie = new Movie({title: 'Inception'});\n   * const title = movie.getAttribute('title');\n   *\n   * title.getValue(); // => 'Inception'\n   * title.validate(); // All good!\n   * title.setValue('');\n   * title.validate(); // => Error {failedValidators: [{validator: ..., path: ''}]}\n   * ```\n   *\n   * @category Validation\n   */\n  validate(attributeSelector: AttributeSelector = true) {\n    const failedValidators = this.runValidators(attributeSelector);\n\n    if (failedValidators.length === 0) {\n      return;\n    }\n\n    const details = failedValidators\n      .map(({validator, path}) => `${validator.getMessage()} (path: '${path}')`)\n      .join(', ');\n\n    let displayMessage: string | undefined;\n\n    for (const {validator} of failedValidators) {\n      const message = validator.getMessage({generateIfMissing: false});\n\n      if (message !== undefined) {\n        displayMessage = message;\n        break;\n      }\n    }\n\n    throwError(\n      `The following error(s) occurred while validating the attribute '${this.getName()}': ${details}`,\n      {displayMessage, failedValidators}\n    );\n  }\n\n  /**\n   * Returns whether the value of the attribute is valid.\n   *\n   * @param [attributeSelector] In case the value of the attribute is a component, your can pass an [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the component's attributes to be validated (default: `true`, which means that all the component's attributes will be validated).\n   *\n   * @returns A boolean.\n   *\n   * @example\n   * ```\n   * // See the `title` definition in the `validate()` example\n   *\n   * title.getValue(); // => 'Inception'\n   * title.isValid(); // => true\n   * title.setValue('');\n   * title.isValid(); // => false\n   * ```\n   *\n   * @category Validation\n   */\n  isValid(attributeSelector: AttributeSelector = true) {\n    const failedValidators = this.runValidators(attributeSelector);\n\n    return failedValidators.length === 0;\n  }\n\n  /**\n   * Runs the validators with the value of the attribute.\n   *\n   * @param [attributeSelector] In case the value of the attribute is a component, your can pass an [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the component's attributes to be validated (default: `true`, which means that all the component's attributes will be validated).\n   *\n   * @returns An array containing the validators that have failed. Each item is a plain object composed of a `validator` (a [`Validator`](https://layrjs.com/docs/v2/reference/validator) instance) and a `path` (a string representing the path of the attribute containing the validator that has failed).\n   *\n   * @example\n   * ```\n   * // See the `title` definition in the `validate()` example\n   *\n   * title.getValue(); // => 'Inception'\n   * title.runValidators(); // => []\n   * title.setValue('');\n   * title.runValidators(); // => [{validator: ..., path: ''}]\n   * ```\n   *\n   * @category Validation\n   */\n  runValidators(attributeSelector: AttributeSelector = true) {\n    if (!this.isSet()) {\n      throw new Error(`Cannot run the validators of an unset attribute (${this.describe()})`);\n    }\n\n    const failedValidators = this.getValueType().runValidators(this.getValue(), attributeSelector);\n\n    return failedValidators;\n  }\n\n  // === Observability ===\n\n  /**\n   * See the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) class.\n   *\n   * @category Observability\n   */\n\n  // === Introspection ===\n\n  introspect() {\n    const introspectedAttribute = super.introspect() as IntrospectedAttribute;\n\n    if (introspectedAttribute === undefined) {\n      return undefined;\n    }\n\n    const exposure = this.getExposure();\n    const getIsExposed = exposure !== undefined ? hasOwnProperty(exposure, 'get') : false;\n    const setIsExposed = exposure !== undefined ? hasOwnProperty(exposure, 'set') : false;\n\n    if (getIsExposed && this.isSet()) {\n      introspectedAttribute.value = this.getValue();\n    }\n\n    if (setIsExposed) {\n      const defaultValue = this.getDefault();\n\n      if (defaultValue !== undefined) {\n        introspectedAttribute.default = defaultValue;\n      }\n    }\n\n    Object.assign(introspectedAttribute, this.getValueType().introspect());\n\n    return introspectedAttribute;\n  }\n\n  static unintrospect(introspectedAttribute: IntrospectedAttribute) {\n    const {\n      value: initialValue,\n      default: defaultValue,\n      valueType,\n      validators,\n      items,\n      ...introspectedProperty\n    } = introspectedAttribute;\n\n    const hasInitialValue = 'value' in introspectedAttribute;\n    const hasDefaultValue = 'default' in introspectedAttribute;\n\n    const {name, options} = super.unintrospect(introspectedProperty) as UnintrospectedAttribute;\n\n    if (hasInitialValue) {\n      options.value = initialValue;\n    }\n\n    if (hasDefaultValue) {\n      options.default = defaultValue;\n    }\n\n    Object.assign(options, unintrospectValueType({valueType, validators, items}));\n\n    return {name, options};\n  }\n\n  // === Utilities ===\n\n  static isAttribute(value: any): value is Attribute {\n    return isAttributeInstance(value);\n  }\n\n  describeType() {\n    return 'attribute';\n  }\n}\n\n/**\n * Returns whether the specified value is an `Attribute` class.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isAttributeClass(value: any): value is typeof Attribute {\n  return typeof value?.isAttribute === 'function';\n}\n\n/**\n * Returns whether the specified value is an `Attribute` instance.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isAttributeInstance(value: any): value is Attribute {\n  return isAttributeClass(value?.constructor) === true;\n}\n"
  },
  {
    "path": "packages/component/src/properties/identifier-attribute.test.ts",
    "content": "import {Component} from '../component';\nimport {IdentifierAttribute, isIdentifierAttributeInstance} from './identifier-attribute';\nimport {isStringValueTypeInstance, isNumberValueTypeInstance} from './value-types';\n\ndescribe('IdentifierAttribute', () => {\n  test('Creation', async () => {\n    class Movie extends Component {}\n\n    let idAttribute = new IdentifierAttribute('id', Movie.prototype);\n\n    expect(isIdentifierAttributeInstance(idAttribute)).toBe(true);\n    expect(idAttribute.getName()).toBe('id');\n    expect(idAttribute.getParent()).toBe(Movie.prototype);\n    expect(isStringValueTypeInstance(idAttribute.getValueType())).toBe(true);\n\n    idAttribute = new IdentifierAttribute('id', Movie.prototype, {valueType: 'number'});\n\n    expect(isIdentifierAttributeInstance(idAttribute)).toBe(true);\n    expect(idAttribute.getName()).toBe('id');\n    expect(idAttribute.getParent()).toBe(Movie.prototype);\n    expect(isNumberValueTypeInstance(idAttribute.getValueType())).toBe(true);\n\n    expect(() => new IdentifierAttribute('id', Movie.prototype, {valueType: 'boolean'})).toThrow(\n      \"The type of an identifier attribute must be 'string' or 'number' (attribute: 'Movie.prototype.id', specified type: 'boolean')\"\n    );\n\n    expect(() => new IdentifierAttribute('id', Movie.prototype, {valueType: 'string?'})).toThrow(\n      \"The value of an identifier attribute cannot be optional (attribute: 'Movie.prototype.id', specified type: 'string?')\"\n    );\n  });\n});\n"
  },
  {
    "path": "packages/component/src/properties/identifier-attribute.ts",
    "content": "import {hasOwnProperty} from 'core-helpers';\n\nimport type {Component} from '../component';\nimport {Attribute, AttributeOptions, ValueSource} from './attribute';\nimport {isComponentInstance} from '../utilities';\n\nexport type IdentifierValue = string | number;\n\n/**\n * *Inherits from [`Attribute`](https://layrjs.com/docs/v2/reference/attribute).*\n *\n * A base class from which [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute) and [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute) are constructed. Unless you build a custom identifier attribute class, you probably won't have to use this class directly.\n */\nexport class IdentifierAttribute extends Attribute {\n  constructor(name: string, parent: Component, options: AttributeOptions = {}) {\n    if (!isComponentInstance(parent)) {\n      throw new Error(\n        `Cannot create a primary identifier attribute with a parent that is not a component instance (property: '${name}')`\n      );\n    }\n\n    super(name, parent, options);\n  }\n\n  getParent() {\n    return super.getParent() as Component;\n  }\n\n  // === Options ===\n\n  setOptions(options: AttributeOptions = {}) {\n    const {valueType = 'string'} = options;\n\n    if (valueType.endsWith('?')) {\n      throw new Error(\n        `The value of an identifier attribute cannot be optional (${this.describe()}, specified type: '${valueType}')`\n      );\n    }\n\n    if (valueType !== 'string' && valueType !== 'number') {\n      throw new Error(\n        `The type of an identifier attribute must be 'string' or 'number' (${this.describe()}, specified type: '${valueType}')`\n      );\n    }\n\n    super.setOptions({...options, valueType});\n  }\n\n  // === Value ===\n\n  getValue(options: {throwIfUnset?: boolean; autoFork?: boolean} = {}) {\n    return super.getValue(options) as IdentifierValue | undefined;\n  }\n\n  setValue(value: IdentifierValue, {source = 'local'}: {source?: ValueSource} = {}) {\n    if (hasOwnProperty(this, '_ignoreNextSetValueCall')) {\n      delete this._ignoreNextSetValueCall;\n      return {previousValue: undefined, newValue: undefined};\n    }\n\n    const {previousValue, newValue} = super.setValue(value, {source}) as {\n      previousValue: IdentifierValue | undefined;\n      newValue: IdentifierValue | undefined;\n    };\n\n    const parent = this.getParent();\n    const identityMap = parent.constructor.getIdentityMap();\n    identityMap.updateComponent(parent, this.getName(), {previousValue, newValue});\n\n    return {previousValue, newValue};\n  }\n\n  unsetValue() {\n    if (!this.isSet()) {\n      return {previousValue: undefined};\n    }\n\n    const {previousValue} = super.unsetValue() as {previousValue: IdentifierValue | undefined};\n\n    const parent = this.getParent();\n    const identityMap = parent.constructor.getIdentityMap();\n    identityMap.updateComponent(parent, this.getName(), {previousValue, newValue: undefined});\n\n    return {previousValue};\n  }\n\n  // === Utilities ===\n\n  static isIdentifierAttribute(value: any): value is IdentifierAttribute {\n    return isIdentifierAttributeInstance(value);\n  }\n}\n\n/**\n * Returns whether the specified value is an `IdentifierAttribute` class.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isIdentifierAttributeClass(value: any): value is typeof IdentifierAttribute {\n  return typeof value?.isIdentifierAttribute === 'function';\n}\n\n/**\n * Returns whether the specified value is an `IdentifierAttribute` instance.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isIdentifierAttributeInstance(value: any): value is IdentifierAttribute {\n  return isIdentifierAttributeClass(value?.constructor) === true;\n}\n"
  },
  {
    "path": "packages/component/src/properties/index.ts",
    "content": "export * from './attribute';\nexport * from './attribute-selector';\nexport * from './identifier-attribute';\nexport * from './method';\nexport * from './primary-identifier-attribute';\nexport * from './property';\nexport * from './secondary-identifier-attribute';\nexport * from './value-types';\n"
  },
  {
    "path": "packages/component/src/properties/method.test.ts",
    "content": "import {MINUTE, HOUR, DAY} from '@layr/utilities';\n\nimport {Component} from '../component';\nimport {Method} from './method';\n\ndescribe('Method', () => {\n  test('Creation', async () => {\n    class Movie extends Component {}\n\n    const method = new Method('find', Movie);\n\n    expect(Method.isMethod(method)).toBe(true);\n    expect(method.getName()).toBe('find');\n    expect(method.getParent()).toBe(Movie);\n  });\n\n  test('Scheduling', async () => {\n    class Movie extends Component {}\n\n    const findMethod = new Method('find', Movie);\n\n    expect(findMethod.getScheduling()).toBe(undefined);\n\n    const updateRatingsMethod = new Method('updateRatings', Movie, {schedule: {rate: 1 * HOUR}});\n\n    expect(updateRatingsMethod.getScheduling()).toEqual({rate: 1 * HOUR});\n\n    expect(() => {\n      new Method('play', Movie.prototype, {schedule: {rate: 1 * DAY}});\n    }).toThrow(\"Only static methods can be scheduled (method: 'Movie.prototype.play')\");\n  });\n\n  test('Queueing', async () => {\n    class Movie extends Component {}\n\n    const findMethod = new Method('find', Movie);\n\n    expect(findMethod.getQueueing()).toBe(undefined);\n\n    const updateRatingsMethod = new Method('updateRatings', Movie, {queue: true});\n\n    expect(updateRatingsMethod.getQueueing()).toBe(true);\n\n    const generateThumbnailMethod = new Method('generateThumbnail', Movie.prototype, {queue: true});\n\n    expect(generateThumbnailMethod.getQueueing()).toBe(true);\n  });\n\n  test('Maximum duration', async () => {\n    class Movie extends Component {}\n\n    const findMethod = new Method('find', Movie);\n\n    expect(findMethod.getMaximumDuration()).toBe(undefined);\n\n    const updateRatingsMethod = new Method('updateRatings', Movie, {\n      queue: true,\n      maximumDuration: 1 * MINUTE\n    });\n\n    expect(updateRatingsMethod.getMaximumDuration()).toBe(1 * MINUTE);\n  });\n});\n"
  },
  {
    "path": "packages/component/src/properties/method.ts",
    "content": "import type {Component} from '../component';\nimport {Property, PropertyOptions, IntrospectedProperty} from './property';\nimport {isComponentClass} from '../utilities';\n\nexport type IntrospectedMethod = IntrospectedProperty;\n\nexport type MethodOptions = PropertyOptions & {\n  schedule?: MethodScheduling;\n  queue?: MethodQueueing;\n  maximumDuration?: number;\n};\n\nexport type MethodScheduling = {rate: number} | false;\n\nexport type MethodQueueing = boolean;\n\n/**\n * *Inherits from [`Property`](https://layrjs.com/docs/v2/reference/property).*\n *\n * A `Method` represents a method of a [Component](https://layrjs.com/docs/v2/reference/component) class, prototype, or instance. It plays the role of a regular JavaScript method, but brings some extra features such as remote invocation, scheduled execution, or queuing.\n *\n * #### Usage\n *\n * Typically, you define a `Method` using the [`@method()`](https://layrjs.com/docs/v2/reference/component#method-decorator) decorator.\n *\n * For example, here is how you would define a `Movie` class with some methods:\n *\n * ```\n * import {Component, method} from '﹫layr/component';\n *\n * class Movie extends Component {\n *   // Class method\n *   ﹫method() static getConfig() {\n *     // ...\n *   }\n *\n *   // Instance method\n *   ﹫method() play() {\n *     // ...\n *   }\n * }\n * ```\n *\n * Then you can call a method like you would normally do with regular JavaScript:\n *\n * ```\n * Movie.getConfig();\n *\n * const movie = new Movie({title: 'Inception'});\n * movie.play();\n * ```\n *\n * So far, you may wonder what is the point of defining methods this way. By itself the [`@method()`](https://layrjs.com/docs/v2/reference/component#method-decorator) decorator, except for creating a `Method` instance under the hood, doesn't provide much benefit.\n *\n * The trick is that since you have a `Method`, you also have a [`Property`](https://layrjs.com/docs/v2/reference/property) (because `Method` inherits from `Property`), and properties can be exposed to remote access thanks to the [`@expose()`](https://layrjs.com/docs/v2/reference/component#expose-decorator) decorator.\n *\n * So here is how you would expose the `Movie` methods:\n *\n * ```\n * import {Component, method} from '﹫layr/component';\n *\n * class Movie extends Component {\n *   // Exposed class method\n *   ﹫expose({call: true}) ﹫method() static getConfig() {\n *     // ...\n *   }\n *\n *   // Exposed instance method\n *   ﹫expose({call: true}) ﹫method() play() {\n *     // ...\n *   }\n * }\n * ```\n *\n * Now that you have some exposed methods, you can call them remotely in the same way you would do locally:\n *\n * ```\n * Movie.getConfig(); // Executed remotely\n *\n * const movie = new Movie({title: 'Inception'});\n * movie.play();  // Executed remotely\n * ```\n *\n * In addition, you can easily take advantage of some powerful features offered by [`Methods`](https://layrjs.com/docs/v2/reference/method). For example, here is how you would define a method that will be automatically executed every hour:\n *\n * ```\n * class Application extends Component {\n *   ﹫method({schedule: {rate: 60 * 60 * 1000}}) static async runHourlyTask() {\n *     // Do something every hour...\n *   }\n * }\n * ```\n *\n * And here is how you would define a method that will be executed in background with a maximum duration of 5 minutes:\n *\n * ```\n * class Email extends Component {\n *   ﹫method({queue: true, maximumDuration: 5 * 60 * 1000}) async send() {\n *     // Do something in background\n *   }\n * }\n * ```\n */\nexport class Method extends Property {\n  _methodBrand!: void;\n\n  /**\n   * Creates an instance of [`Method`](https://layrjs.com/docs/v2/reference/method). Typically, instead of using this constructor, you would rather use the [`@method()`](https://layrjs.com/docs/v2/reference/component#method-decorator) decorator.\n   *\n   * @param name The name of the method.\n   * @param parent The component class, prototype, or instance that owns the method.\n   * @param [options.exposure] A [`PropertyExposure`](https://layrjs.com/docs/v2/reference/property#property-exposure-type) object specifying how the method should be exposed to remote calls.\n   * @param [options.schedule] A [`MethodScheduling`](https://layrjs.com/docs/v2/reference/method#method-scheduling-type) object specifying how the method should be scheduled for automatic execution. Note that only static methods can be scheduled.\n   * @param [options.queue] A boolean specifying whether the method should be executed in background.\n   * @param [options.maximumDuration] A number specifying the maximum duration of the method in milliseconds. Note that the actual duration of the method execution is not currently enforced. The purpose of this option is to help configuring the deployment of serverless functions. For example, in case of deployment to [AWS Lambda](https://aws.amazon.com/lambda/), this option will affect the `timeout` property of the generated Lambda function.\n   *\n   * @returns The [`Method`](https://layrjs.com/docs/v2/reference/method) instance that was created.\n   *\n   * @example\n   * ```\n   * import {Component, Method} from '﹫layr/component';\n   *\n   * class Movie extends Component {}\n   *\n   * const play = new Method('play', Movie.prototype, {exposure: {call: true}});\n   *\n   * play.getName(); // => 'play'\n   * play.getParent(); // => Movie.prototype\n   * play.getExposure(); // => {call: true}\n   * ```\n   *\n   * @category Creation\n   */\n  constructor(name: string, parent: typeof Component | Component, options: MethodOptions = {}) {\n    super(name, parent, options);\n  }\n\n  // === Options ===\n\n  setOptions(options: MethodOptions = {}) {\n    const {schedule, queue, maximumDuration, ...otherOptions} = options;\n\n    super.setOptions(otherOptions);\n\n    if (schedule !== undefined) {\n      this.setScheduling(schedule);\n    }\n\n    if (queue !== undefined) {\n      this.setQueueing(queue);\n    }\n\n    if (maximumDuration !== undefined) {\n      this.setMaximumDuration(maximumDuration);\n    }\n  }\n\n  // === Property Methods ===\n\n  /**\n   * See the methods that are inherited from the [`Property`](https://layrjs.com/docs/v2/reference/property#basic-methods) class.\n   *\n   * @category Methods\n   */\n\n  // === Scheduling ===\n\n  _scheduling?: MethodScheduling;\n\n  /**\n   * If the method is scheduled for automatic execution, returns a [`MethodScheduling`](https://layrjs.com/docs/v2/reference/method#method-scheduling-type) object. Otherwise, returns `undefined`.\n   *\n   * @returns A [`MethodScheduling`](https://layrjs.com/docs/v2/reference/method#method-scheduling-type) object or `undefined`.\n   *\n   * @example\n   * ```\n   * runHourlyTaskMethod.getScheduling(); // => {rate: 60 * 60 * 1000}\n   * regularMethod.getScheduling(); // => undefined\n   * ```\n   *\n   * @category Scheduling\n   */\n  getScheduling() {\n    return this._scheduling;\n  }\n\n  /**\n   * Sets how the method should be scheduled for automatic execution. Note that only static methods can be scheduled.\n   *\n   * @param scheduling A [`MethodScheduling`](https://layrjs.com/docs/v2/reference/method#method-scheduling-type) object.\n   *\n   * @example\n   * ```\n   * runHourlyTaskMethod.setScheduling({rate: 60 * 60 * 1000});\n   * ```\n   *\n   * @category Scheduling\n   */\n  setScheduling(scheduling: MethodScheduling | undefined) {\n    if (!isComponentClass(this.getParent())) {\n      throw new Error(`Only static methods can be scheduled (${this.describe()})`);\n    }\n\n    this._scheduling = scheduling;\n  }\n\n  /**\n   * @typedef MethodScheduling\n   *\n   * A `MethodScheduling` is a plain object specifying how a method is scheduled for automatic execution. The shape of the object is `{rate: number}` where `rate` is the execution frequency expressed in milliseconds.\n   *\n   * @example\n   * ```\n   * {rate: 60 * 1000} // Every minute\n   * {rate: 60 * 60 * 1000} // Every hour\n   * {rate: 24 * 60 * 60 * 1000} // Every day\n   * ```\n   *\n   * @category Scheduling\n   */\n\n  // === Queueing ===\n\n  _queueing?: MethodQueueing;\n\n  /**\n   * Returns `true` if the method should be executed in background. Otherwise, returns `undefined`.\n   *\n   * @returns A boolean or `undefined`.\n   *\n   * @example\n   * ```\n   * backgroundMethod.getQueueing(); // => true\n   * regularMethod.getQueueing(); // => undefined\n   * ```\n   *\n   * @category Queueing\n   */\n  getQueueing() {\n    return this._queueing;\n  }\n\n  /**\n   * Sets whether the method should be executed in background.\n   *\n   * @param queueing Pass `true` to specify that the method should be executed in background. Otherwise, you can pass `false` or `undefined`.\n   *\n   * @example\n   * ```\n   * backgroundMethod.setQueueing(true);\n   * ```\n   *\n   * @category Queueing\n   */\n  setQueueing(queueing: MethodQueueing | undefined) {\n    this._queueing = queueing;\n  }\n\n  // === Maximum Duration ===\n\n  _maximumDuration?: number;\n\n  /**\n   * Returns a number representing the maximum duration of the method in milliseconds or `undefined` if the method has no maximum duration.\n   *\n   * @returns A number or `undefined`.\n   *\n   * @example\n   * ```\n   * backgroundMethod.getMaximumDuration(); // => 5 * 60 * 1000 (5 minutes)\n   * regularMethod.getMaximumDuration(); // => undefined\n   * ```\n   *\n   * @category Maximum Duration\n   */\n  getMaximumDuration() {\n    return this._maximumDuration;\n  }\n\n  /**\n   * Sets the maximum duration of the method in milliseconds. Alternatively, you can pass `undefined` to indicate that the method has no maximum duration.\n   *\n   * @param maximumDuration A number or `undefined`.\n   *\n   * @example\n   * ```\n   * backgroundMethod.setMaximumDuration(5 * 60 * 1000); // 5 minutes\n   * ```\n   *\n   * @category Maximum Duration\n   */\n  setMaximumDuration(maximumDuration: number | undefined) {\n    this._maximumDuration = maximumDuration;\n  }\n\n  // === Utilities ===\n\n  static isMethod(value: any): value is Method {\n    return isMethodInstance(value);\n  }\n\n  describeType() {\n    return 'method';\n  }\n}\n\n/**\n * Returns whether the specified value is a `Method` class.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isMethodClass(value: any): value is typeof Method {\n  return typeof value?.isMethod === 'function';\n}\n\n/**\n * Returns whether the specified value is a `Method` instance.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isMethodInstance(value: any): value is Method {\n  return isMethodClass(value?.constructor) === true;\n}\n"
  },
  {
    "path": "packages/component/src/properties/primary-identifier-attribute.test.ts",
    "content": "import {Component} from '../component';\nimport {\n  PrimaryIdentifierAttribute,\n  isPrimaryIdentifierAttributeInstance,\n  primaryIdentifierAttributeStringDefaultValue\n} from './primary-identifier-attribute';\nimport {isStringValueTypeInstance, isNumberValueTypeInstance} from './value-types';\n\ndescribe('PrimaryIdentifierAttribute', () => {\n  test('Creation', async () => {\n    class Movie extends Component {}\n\n    let idAttribute = new PrimaryIdentifierAttribute('id', Movie.prototype);\n\n    expect(isPrimaryIdentifierAttributeInstance(idAttribute)).toBe(true);\n    expect(idAttribute.getName()).toBe('id');\n    expect(idAttribute.getParent()).toBe(Movie.prototype);\n    expect(isStringValueTypeInstance(idAttribute.getValueType())).toBe(true);\n    expect(idAttribute.getDefault()).toBe(primaryIdentifierAttributeStringDefaultValue);\n\n    idAttribute = new PrimaryIdentifierAttribute('id', Movie.prototype, {valueType: 'number'});\n\n    expect(isPrimaryIdentifierAttributeInstance(idAttribute)).toBe(true);\n    expect(idAttribute.getName()).toBe('id');\n    expect(idAttribute.getParent()).toBe(Movie.prototype);\n    expect(isNumberValueTypeInstance(idAttribute.getValueType())).toBe(true);\n    expect(idAttribute.getDefault()).toBeUndefined();\n  });\n\n  test('Value', async () => {\n    class Movie extends Component {}\n\n    const idAttribute = new PrimaryIdentifierAttribute('id', Movie.prototype);\n\n    expect(idAttribute.isSet()).toBe(false);\n\n    idAttribute.setValue('abc123');\n\n    expect(idAttribute.getValue()).toBe('abc123');\n\n    idAttribute.setValue('abc123');\n\n    expect(idAttribute.getValue()).toBe('abc123');\n\n    expect(() => idAttribute.setValue('xyz789')).toThrow(\n      \"The value of a primary identifier attribute cannot be modified (attribute: 'Movie.prototype.id')\"\n    );\n  });\n\n  test('Default value', async () => {\n    class Movie extends Component {}\n\n    const idAttribute = new PrimaryIdentifierAttribute('id', Movie.prototype);\n\n    const id = idAttribute.evaluateDefault() as string;\n\n    expect(typeof id).toBe('string');\n    expect(id.length >= 25).toBe(true);\n  });\n\n  test('Introspection', async () => {\n    class Movie extends Component {}\n\n    expect(\n      new PrimaryIdentifierAttribute('id', Movie.prototype, {\n        exposure: {get: true, set: true}\n      }).introspect()\n    ).toStrictEqual({\n      name: 'id',\n      type: 'PrimaryIdentifierAttribute',\n      valueType: 'string',\n      default: primaryIdentifierAttributeStringDefaultValue,\n      exposure: {get: true, set: true}\n    });\n\n    expect(\n      new PrimaryIdentifierAttribute('id', Movie.prototype, {\n        valueType: 'number',\n        exposure: {get: true, set: true}\n      }).introspect()\n    ).toStrictEqual({\n      name: 'id',\n      type: 'PrimaryIdentifierAttribute',\n      valueType: 'number',\n      exposure: {get: true, set: true}\n    });\n  });\n\n  test('Unintrospection', async () => {\n    expect(\n      PrimaryIdentifierAttribute.unintrospect({\n        name: 'id',\n        type: 'PrimaryIdentifierAttribute',\n        valueType: 'string',\n        default: primaryIdentifierAttributeStringDefaultValue,\n        exposure: {get: true}\n      })\n    ).toEqual({\n      name: 'id',\n      options: {\n        valueType: 'string',\n        default: primaryIdentifierAttributeStringDefaultValue,\n        exposure: {get: true}\n      }\n    });\n\n    expect(\n      PrimaryIdentifierAttribute.unintrospect({\n        name: 'id',\n        type: 'PrimaryIdentifierAttribute',\n        valueType: 'number',\n        exposure: {get: true}\n      })\n    ).toEqual({\n      name: 'id',\n      options: {\n        valueType: 'number',\n        exposure: {get: true}\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "packages/component/src/properties/primary-identifier-attribute.ts",
    "content": "import {hasOwnProperty} from 'core-helpers';\n\nimport type {Component} from '../component';\nimport type {AttributeOptions, ValueSource} from './attribute';\nimport {IdentifierAttribute, IdentifierValue} from './identifier-attribute';\nimport {isComponentInstance, ensureComponentClass} from '../utilities';\n\n/**\n * *Inherits from [`IdentifierAttribute`](https://layrjs.com/docs/v2/reference/identifier-attribute).*\n *\n * A `PrimaryIdentifierAttribute` is a special kind of attribute that uniquely identify a [Component](https://layrjs.com/docs/v2/reference/component) instance.\n *\n * A `Component` can have only one `PrimaryIdentifierAttribute`. To define a `Component` with more than one identifier, you can add some [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute) in addition to the `PrimaryIdentifierAttribute`.\n *\n * Another characteristic of a `PrimaryIdentifierAttribute` is that its value is immutable (i.e., once set it cannot change). This ensures a stable identity of the components across the different layers of an app (e.g., frontend, backend, and database).\n *\n * When a `Component` has a `PrimaryIdentifierAttribute`, its instances are managed by an [`IdentityMap`](https://layrjs.com/docs/v2/reference/identity-map) ensuring that there can only be one instance with a specific identifier.\n *\n * #### Usage\n *\n * Typically, you create a `PrimaryIdentifierAttribute` and associate it to a component prototype using the [`@primaryIdentifier()`](https://layrjs.com/docs/v2/reference/component#primary-identifier-decorator) decorator.\n *\n * For example, here is how you would define a `Movie` class with an `id` primary identifier attribute:\n *\n * ```\n * // JS\n *\n * import {Component, primaryIdentifier, attribute} from '﹫layr/component';\n *\n * class Movie extends Component {\n *   // An auto-generated 'string' primary identifier attribute\n *   ﹫primaryIdentifier() id;\n *\n *   // A regular attribute\n *   ﹫attribute('string') title;\n * }\n * ```\n *\n * ```\n * // TS\n *\n * import {Component, primaryIdentifier, attribute} from '﹫layr/component';\n *\n * class Movie extends Component {\n *   // An auto-generated 'string' primary identifier attribute\n *   ﹫primaryIdentifier() id!: string;\n *\n *   // A regular attribute\n *   ﹫attribute('string') title!: string;\n * }\n * ```\n *\n * Then, to create a `Movie` instance, you would do something like:\n *\n * ```\n * const movie = new Movie({title: 'Inception'});\n *\n * movie.id; // => 'ck41vli1z00013h5xx1esffyn'\n * movie.title; // => 'Inception'\n * ```\n *\n * Note that we didn't have to specify a value for the `id` attribute; it was automatically generated (using the [`Component.generateId()`](https://layrjs.com/docs/v2/reference/component#generate-id-class-method) method under the hood).\n *\n * To create a `Movie` instance with an `id` of your choice, just do:\n *\n * ```\n * const movie = new Movie({id: 'abc123', title: 'Inception'});\n *\n * movie.id; // => 'abc123'\n * movie.title; // => 'Inception'\n * ```\n *\n * As mentioned previously, when a component has a primary identifier attribute, all its instances are managed by an [`IdentityMap`](https://layrjs.com/docs/v2/reference/identity-map) ensuring that there is only one instance with a specific identifier.\n *\n * So, since we previously created a `Movie` with `'abc123'` as primary identifier, we cannot create another `Movie` with the same primary identifier:\n *\n * ```\n * new Movie({id: 'abc123', title: 'Inception 2'}); // => Error\n * ```\n *\n * `PrimaryIdentifierAttribute` values are usually of type `'string'` (the default), but you can also have values of type `'number'`:\n *\n * ```\n * // JS\n *\n * import {Component, primaryIdentifier} from '﹫layr/component';\n *\n * class Movie extends Component {\n *   // An auto-generated 'number' primary identifier attribute\n *   ﹫primaryIdentifier('number') id = Math.random();\n * }\n * ```\n *\n * ```\n * // TS\n *\n * import {Component, primaryIdentifier} from '﹫layr/component';\n *\n * class Movie extends Component {\n *   // An auto-generated 'number' primary identifier attribute\n *   ﹫primaryIdentifier('number') id = Math.random();\n * }\n * ```\n */\nexport class PrimaryIdentifierAttribute extends IdentifierAttribute {\n  /**\n   * Creates an instance of [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute). Typically, instead of using this constructor, you would rather use the [`@primaryIdentifier()`](https://layrjs.com/docs/v2/reference/component#primary-identifier-decorator) decorator.\n   *\n   * @param name The name of the attribute.\n   * @param parent The component prototype that owns the attribute.\n   * @param [options.valueType] A string specifying the type of values the attribute can store. It can be either `'string'` or `'number'` (default: `'string'`).\n   * @param [options.default] A function returning the default value of the attribute (default when `valueType` is `'string'`: `function () { return this.constructor.generateId() }`).\n   * @param [options.validators] An array of [validators](https://layrjs.com/docs/v2/reference/validator) for the value of the attribute.\n   * @param [options.exposure] A [`PropertyExposure`](https://layrjs.com/docs/v2/reference/property#property-exposure-type) object specifying how the attribute should be exposed to remote access.\n   *\n   * @returns The [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute) instance that was created.\n   *\n   * @example\n   * ```\n   * import {Component, PrimaryIdentifierAttribute} from '﹫layr/component';\n   *\n   * class Movie extends Component {}\n   *\n   * const id = new PrimaryIdentifierAttribute('id', Movie.prototype);\n   *\n   * id.getName(); // => 'id'\n   * id.getParent(); // => Movie.prototype\n   * id.getValueType().toString(); // => 'string'\n   * id.getDefaultValue(); // => function () { return this.constructor.generateId() }`\n   * ```\n   *\n   * @category Creation\n   */\n  constructor(name: string, parent: Component, options: AttributeOptions = {}) {\n    if (\n      isComponentInstance(parent) &&\n      parent.hasPrimaryIdentifierAttribute() &&\n      parent.getPrimaryIdentifierAttribute().getName() !== name\n    ) {\n      throw new Error(\n        `The component '${ensureComponentClass(\n          parent\n        ).getComponentName()}' already has a primary identifier attribute`\n      );\n    }\n\n    super(name, parent, options);\n  }\n\n  // === Options ===\n\n  setOptions(options: AttributeOptions = {}) {\n    let {valueType = 'string', default: defaultValue} = options;\n\n    if (valueType === 'string' && defaultValue === undefined) {\n      defaultValue = primaryIdentifierAttributeStringDefaultValue;\n    }\n\n    super.setOptions({...options, default: defaultValue});\n  }\n\n  // === Property Methods ===\n\n  /**\n   * See the methods that are inherited from the [`Property`](https://layrjs.com/docs/v2/reference/property#basic-methods) class.\n   *\n   * @category Property Methods\n   */\n\n  // === Attribute Methods ===\n\n  /**\n   * See the methods that are inherited from the [`Attribute`](https://layrjs.com/docs/v2/reference/attribute#value-type) class.\n   *\n   * @category Attribute Methods\n   */\n\n  // === Value ===\n\n  setValue(value: IdentifierValue, {source = 'local'}: {source?: ValueSource} = {}) {\n    if (hasOwnProperty(this, '_ignoreNextSetValueCall')) {\n      delete this._ignoreNextSetValueCall;\n      return {previousValue: undefined, newValue: undefined};\n    }\n\n    const previousValue = this.getValue({throwIfUnset: false, autoFork: false});\n\n    if (previousValue !== undefined && value !== previousValue) {\n      throw new Error(\n        `The value of a primary identifier attribute cannot be modified (${this.describe()})`\n      );\n    }\n\n    return super.setValue(value, {source});\n  }\n\n  // === Observability ===\n\n  /**\n   * See the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) class.\n   *\n   * @category Observability\n   */\n\n  // === Utilities ===\n\n  static isPrimaryIdentifierAttribute(value: any): value is PrimaryIdentifierAttribute {\n    return isPrimaryIdentifierAttributeInstance(value);\n  }\n}\n\n/**\n * Returns whether the specified value is a `PrimaryIdentifierAttribute` class.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isPrimaryIdentifierAttributeClass(\n  value: any\n): value is typeof PrimaryIdentifierAttribute {\n  return typeof value?.isPrimaryIdentifierAttribute === 'function';\n}\n\n/**\n * Returns whether the specified value is a `PrimaryIdentifierAttribute` instance.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isPrimaryIdentifierAttributeInstance(\n  value: any\n): value is PrimaryIdentifierAttribute {\n  return isPrimaryIdentifierAttributeClass(value?.constructor) === true;\n}\n\nexport const primaryIdentifierAttributeStringDefaultValue = (function () {\n  // Makes the function anonymous to make it a bit lighter when serialized\n  return function (this: Component) {\n    return this.constructor.generateId();\n  };\n})();\n"
  },
  {
    "path": "packages/component/src/properties/property.test.ts",
    "content": "import {Component} from '../component';\nimport {Property, PropertyOperationSetting} from './property';\n\ndescribe('Property', () => {\n  test('Creation', async () => {\n    class Movie extends Component {}\n\n    // @ts-expect-error\n    expect(() => new Property()).toThrow(\n      \"Expected a string, but received a value of type 'undefined'\"\n    );\n\n    // @ts-expect-error\n    expect(() => new Property('title')).toThrow(\n      \"Expected a component class or instance, but received a value of type 'undefined'\"\n    );\n\n    // @ts-expect-error\n    expect(() => new Property('title', {})).toThrow(\n      \"Expected a component class or instance, but received a value of type 'object'\"\n    );\n\n    // @ts-expect-error\n    expect(() => new Property('title', Movie, {unknownOption: 123})).toThrow(\n      \"Did not expect the option 'unknownOption' to exist\"\n    );\n\n    const property = new Property('title', Movie.prototype);\n\n    expect(Property.isProperty(property)).toBe(true);\n    expect(property.getName()).toBe('title');\n    expect(property.getParent()).toBe(Movie.prototype);\n  });\n\n  test('Exposure', async () => {\n    class Movie extends Component {}\n\n    expect(new Property('find', Movie).getExposure()).toBeUndefined();\n\n    expect(new Property('find', Movie, {exposure: {}}).getExposure()).toBeUndefined();\n\n    expect(\n      new Property('find', Movie, {exposure: {call: undefined}}).getExposure()\n    ).toBeUndefined();\n\n    expect(new Property('find', Movie, {exposure: {call: true}}).getExposure()).toStrictEqual({\n      call: true\n    });\n\n    expect(() => new Property('find', Movie, {exposure: {call: false}})).toThrow(\n      'The specified property operation setting (false) is invalid'\n    );\n\n    expect(() => new Property('find', Movie, {exposure: {call: 'admin'}})).toThrow(\n      'The specified property operation setting (\"admin\") is invalid'\n    );\n\n    class Film extends Component {\n      static normalizePropertyOperationSetting(\n        setting: PropertyOperationSetting,\n        {throwIfInvalid = true} = {}\n      ) {\n        const normalizedSetting = super.normalizePropertyOperationSetting(setting, {\n          throwIfInvalid: false\n        });\n\n        if (normalizedSetting !== undefined) {\n          return normalizedSetting;\n        }\n\n        if (typeof setting === 'string') {\n          return setting;\n        }\n\n        if (throwIfInvalid) {\n          throw new Error(\n            `The specified property operation setting (${JSON.stringify(setting)}) is invalid`\n          );\n        }\n\n        return undefined;\n      }\n    }\n\n    expect(new Property('find', Film, {exposure: {call: true}}).getExposure()).toStrictEqual({\n      call: true\n    });\n\n    expect(new Property('find', Film, {exposure: {call: 'admin'}}).getExposure()).toStrictEqual({\n      call: 'admin'\n    });\n\n    expect(() => new Property('find', Film, {exposure: {call: false}})).toThrow(\n      'The specified property operation setting (false) is invalid'\n    );\n  });\n\n  test('Forking', async () => {\n    class Movie extends Component {}\n\n    const property = new Property('title', Movie.prototype);\n\n    expect(property.getName()).toBe('title');\n    expect(property.getParent()).toBe(Movie.prototype);\n\n    const movie = Object.create(Movie.prototype);\n    const propertyFork = property.fork(movie);\n\n    expect(propertyFork.getName()).toBe('title');\n    expect(propertyFork.getParent()).toBe(movie);\n  });\n\n  test('Introspection', async () => {\n    class Movie extends Component {\n      static normalizePropertyOperationSetting(\n        setting: PropertyOperationSetting,\n        {throwIfInvalid = true} = {}\n      ) {\n        const normalizedSetting = super.normalizePropertyOperationSetting(setting, {\n          throwIfInvalid: false\n        });\n\n        if (normalizedSetting !== undefined) {\n          return normalizedSetting;\n        }\n\n        if (typeof setting === 'string') {\n          return setting;\n        }\n\n        if (throwIfInvalid) {\n          throw new Error(\n            `The specified property operation setting (${JSON.stringify(setting)}) is invalid`\n          );\n        }\n\n        return undefined;\n      }\n    }\n\n    expect(new Property('title', Movie.prototype).introspect()).toBeUndefined();\n\n    expect(\n      new Property('title', Movie.prototype, {exposure: {get: true}}).introspect()\n    ).toStrictEqual({name: 'title', type: 'Property', exposure: {get: true}});\n\n    expect(\n      new Property('title', Movie.prototype, {exposure: {get: true, set: 'admin'}}).introspect()\n    ).toStrictEqual({name: 'title', type: 'Property', exposure: {get: true, set: true}});\n  });\n\n  test('Unintrospection', async () => {\n    expect(\n      Property.unintrospect({name: 'title', type: 'Property', exposure: {get: true, set: true}})\n    ).toStrictEqual({\n      name: 'title',\n      options: {exposure: {get: true, set: true}}\n    });\n  });\n});\n"
  },
  {
    "path": "packages/component/src/properties/property.ts",
    "content": "import {possiblyAsync} from 'possibly-async';\nimport {assertIsString, assertNoUnknownOptions, PlainObject, getTypeOf} from 'core-helpers';\nimport mapValues from 'lodash/mapValues';\n\nimport type {Component} from '../component';\nimport {ensureComponentClass, assertIsComponentClassOrInstance} from '../utilities';\n\nexport type PropertyOptions = {\n  exposure?: PropertyExposure;\n};\n\nexport type PropertyExposure = Partial<Record<PropertyOperation, PropertyOperationSetting>>;\n\nexport type PropertyOperation = 'get' | 'set' | 'call';\n\nexport type PropertyOperationSetting = boolean | string | string[];\n\nexport type PropertyFilter = (property: any) => boolean | PromiseLike<boolean>;\nexport type PropertyFilterSync = (property: any) => boolean;\nexport type PropertyFilterAsync = (property: any) => PromiseLike<boolean>;\n\nexport type IntrospectedProperty = {\n  name: string;\n  type: string;\n  exposure?: IntrospectedExposure;\n};\n\nexport type IntrospectedExposure = Partial<Record<PropertyOperation, boolean>>;\n\nexport type UnintrospectedProperty = {\n  name: string;\n  options: {\n    exposure?: UnintrospectedExposure;\n  };\n};\n\nexport type UnintrospectedExposure = Partial<Record<PropertyOperation, true>>;\n\n/**\n * A base class from which classes such as [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) or [`Method`](https://layrjs.com/docs/v2/reference/method) are constructed. Unless you build a custom property class, you probably won't have to use this class directly.\n */\nexport class Property {\n  _name: string;\n  _parent: typeof Component | Component;\n\n  /**\n   * Creates an instance of [`Property`](https://layrjs.com/docs/v2/reference/property).\n   *\n   * @param name The name of the property.\n   * @param parent The component class, prototype, or instance that owns the property.\n   * @param [options.exposure] A [`PropertyExposure`](https://layrjs.com/docs/v2/reference/property#property-exposure-type) object specifying how the property should be exposed to remote access.\n   *\n   * @returns The [`Property`](https://layrjs.com/docs/v2/reference/property) instance that was created.\n   *\n   * @example\n   * ```\n   * import {Component, Property} from '﹫layr/component';\n   *\n   * class Movie extends Component {}\n   *\n   * const titleProperty = new Property('title', Movie.prototype);\n   *\n   * titleProperty.getName(); // => 'title'\n   * titleProperty.getParent(); // => Movie.prototype\n   * ```\n   *\n   * @category Creation\n   */\n  constructor(name: string, parent: typeof Component | Component, options: PropertyOptions = {}) {\n    assertIsString(name);\n    assertIsComponentClassOrInstance(parent);\n\n    this._name = name;\n    this._parent = parent;\n\n    this.setOptions(options);\n\n    this._initialize();\n  }\n\n  _initialize() {}\n\n  /**\n   * Returns the name of the property.\n   *\n   * @returns A string.\n   *\n   * @example\n   * ```\n   * titleProperty.getName(); // => 'title'\n   * ```\n   *\n   * @category Basic Methods\n   */\n  getName() {\n    return this._name;\n  }\n\n  /**\n   * Returns the parent of the property.\n   *\n   * @returns A component class, prototype, or instance.\n   *\n   * @example\n   * ```\n   * titleProperty.getParent(); // => Movie.prototype\n   * ```\n   *\n   * @category Basic Methods\n   */\n  getParent() {\n    return this._parent;\n  }\n\n  // === Options ===\n\n  setOptions(options: PropertyOptions = {}) {\n    const {exposure, ...unknownOptions} = options;\n\n    assertNoUnknownOptions(unknownOptions);\n\n    if (exposure !== undefined) {\n      this.setExposure(exposure);\n    }\n  }\n\n  // === Exposure ===\n\n  _exposure?: PropertyExposure;\n\n  /**\n   * Returns an object specifying how the property is exposed to remote access.\n   *\n   * @returns A [`PropertyExposure`](https://layrjs.com/docs/v2/reference/property#property-exposure-type) object.\n   *\n   * @example\n   * ```\n   * titleProperty.getExposure(); // => {get: true, set: true}\n   * ```\n   *\n   * @category Exposure\n   */\n  getExposure() {\n    return this._exposure;\n  }\n\n  /**\n   * Sets how the property is exposed to remote access.\n   *\n   * @param [exposure] A [`PropertyExposure`](https://layrjs.com/docs/v2/reference/property#property-exposure-type) object.\n   *\n   * @example\n   * ```\n   * titleProperty.setExposure({get: true, set: true});\n   * ```\n   *\n   * @category Exposure\n   */\n  setExposure(exposure = {}) {\n    this._exposure = this._normalizeExposure(exposure);\n  }\n\n  _normalizeExposure(exposure: PropertyExposure) {\n    let normalizedExposure: PlainObject | undefined;\n\n    for (const [operation, setting] of Object.entries(exposure)) {\n      if (setting === undefined) {\n        continue;\n      }\n\n      const normalizedSetting = ensureComponentClass(\n        this._parent\n      ).normalizePropertyOperationSetting(setting);\n\n      if (normalizedSetting === undefined) {\n        continue;\n      }\n\n      if (normalizedExposure === undefined) {\n        normalizedExposure = {};\n      }\n\n      normalizedExposure[operation] = normalizedSetting;\n    }\n\n    return normalizedExposure as PropertyExposure | undefined;\n  }\n\n  /**\n   * Returns whether an operation is allowed on the property.\n   *\n   * @param operation A string representing an operation. Currently supported operations are 'get', 'set', and 'call'.\n   *\n   * @returns A boolean.\n   *\n   * @example\n   * ```\n   * titleProperty.operationIsAllowed('get'); // => true\n   * titleProperty.operationIsAllowed('call'); // => false\n   * ```\n   *\n   * @category Exposure\n   * @possiblyasync\n   */\n  operationIsAllowed(operation: PropertyOperation) {\n    const setting = this._exposure?.[operation];\n\n    if (setting === undefined) {\n      return false;\n    }\n\n    return possiblyAsync(\n      this._parent.resolvePropertyOperationSetting(setting),\n      (resolvedSetting) => resolvedSetting === true\n    );\n  }\n\n  /**\n   * @typedef PropertyExposure\n   *\n   * A `PropertyExposure` is a plain object specifying how a property is exposed to remote access.\n   *\n   * The shape of the object is `{[operation]: permission}` where:\n   *\n   * - `operation` is a string representing the different types of operations (`'get'` and `'set'` for attributes, and `'call'` for methods).\n   * - `permission` is a boolean (or a string or array of strings if the [`WithRoles`](https://layrjs.com/docs/v2/reference/with-roles) mixin is used) specifying whether the operation is allowed or not.\n   *\n   * @example\n   * ```\n   * {get: true, set: true}\n   * {get: 'anyone', set: ['author', 'admin']}\n   * {call: true}\n   * {call: 'admin'}\n   * ```\n   *\n   * @category Exposure\n   */\n\n  // === Forking ===\n\n  fork<T extends Property>(this: T, parent: typeof Component | Component) {\n    const propertyFork = Object.create(this) as T;\n\n    propertyFork._parent = parent;\n\n    propertyFork._initialize();\n\n    return propertyFork;\n  }\n\n  // === Introspection ===\n\n  introspect() {\n    const introspectedExposure = this.introspectExposure();\n\n    if (introspectedExposure === undefined) {\n      return undefined;\n    }\n\n    return {\n      name: this.getName(),\n      type: getTypeOf(this),\n      exposure: introspectedExposure\n    } as IntrospectedProperty;\n  }\n\n  introspectExposure() {\n    const exposure = this.getExposure();\n\n    if (exposure === undefined) {\n      return undefined;\n    }\n\n    // We don't want to expose backend operation settings to the frontend\n    // So if there is a {call: 'admin'} exposure, we want to return {call: true}\n    const introspectedExposure = mapValues(exposure, () => true);\n\n    return introspectedExposure as IntrospectedExposure;\n  }\n\n  static unintrospect(introspectedProperty: IntrospectedProperty) {\n    const {name, type: _type, ...options} = introspectedProperty;\n\n    return {name, options} as UnintrospectedProperty;\n  }\n\n  // === Utilities ===\n\n  static isProperty(value: any): value is Property {\n    return isPropertyInstance(value);\n  }\n\n  describeType() {\n    return 'property';\n  }\n\n  describe() {\n    return `${this.describeType()}: '${this.getParent().describeComponentProperty(\n      this.getName()\n    )}'`;\n  }\n}\n\nexport function isPropertyClass(value: any): value is typeof Property {\n  return typeof value?.isProperty === 'function';\n}\n\nexport function isPropertyInstance(value: any): value is Property {\n  return isPropertyClass(value?.constructor) === true;\n}\n"
  },
  {
    "path": "packages/component/src/properties/secondary-identifier-attribute.test.ts",
    "content": "import {Component} from '../component';\nimport {\n  SecondaryIdentifierAttribute,\n  isSecondaryIdentifierAttributeInstance\n} from './secondary-identifier-attribute';\nimport {isStringValueTypeInstance} from './value-types';\n\ndescribe('SecondaryIdentifierAttribute', () => {\n  test('Creation', async () => {\n    class Movie extends Component {}\n\n    const emailAttribute = new SecondaryIdentifierAttribute('email', Movie.prototype);\n\n    expect(isSecondaryIdentifierAttributeInstance(emailAttribute)).toBe(true);\n    expect(emailAttribute.getName()).toBe('email');\n    expect(emailAttribute.getParent()).toBe(Movie.prototype);\n    expect(isStringValueTypeInstance(emailAttribute.getValueType())).toBe(true);\n  });\n\n  test('Value', async () => {\n    class Movie extends Component {}\n\n    const emailAttribute = new SecondaryIdentifierAttribute('email', Movie.prototype);\n\n    expect(emailAttribute.isSet()).toBe(false);\n\n    emailAttribute.setValue('hi@hello.com');\n\n    expect(emailAttribute.getValue()).toBe('hi@hello.com');\n\n    // Contrary to a primary identifier attribute, the value of a secondary identifier\n    // attribute can be modified\n    emailAttribute.setValue('salut@bonjour.com');\n\n    expect(emailAttribute.getValue()).toBe('salut@bonjour.com');\n  });\n\n  test('Introspection', async () => {\n    class Movie extends Component {}\n\n    expect(\n      new SecondaryIdentifierAttribute('email', Movie.prototype, {\n        valueType: 'string',\n        exposure: {get: true}\n      }).introspect()\n    ).toStrictEqual({\n      name: 'email',\n      type: 'SecondaryIdentifierAttribute',\n      valueType: 'string',\n      exposure: {get: true}\n    });\n  });\n\n  test('Unintrospection', async () => {\n    expect(\n      SecondaryIdentifierAttribute.unintrospect({\n        name: 'email',\n        type: 'SecondaryIdentifierAttribute',\n        valueType: 'string',\n        exposure: {get: true}\n      })\n    ).toEqual({\n      name: 'email',\n      options: {valueType: 'string', exposure: {get: true}}\n    });\n  });\n});\n"
  },
  {
    "path": "packages/component/src/properties/secondary-identifier-attribute.ts",
    "content": "import type {Component} from '../component';\nimport type {AttributeOptions} from './attribute';\nimport {IdentifierAttribute} from './identifier-attribute';\n\n/**\n * *Inherits from [`IdentifierAttribute`](https://layrjs.com/docs/v2/reference/identifier-attribute).*\n *\n * A `SecondaryIdentifierAttribute` is a special kind of attribute that uniquely identify a [Component](https://layrjs.com/docs/v2/reference/component) instance.\n *\n * Contrary to a [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute), you can define more than one `SecondaryIdentifierAttribute` in a `Component`.\n *\n * Another difference with a `PrimaryIdentifierAttribute` is that a `SecondaryIdentifierAttribute` value is mutable (i.e., it can change over time).\n *\n * #### Usage\n *\n * Typically, you create a `SecondaryIdentifierAttribute` and associate it to a component prototype using the [`@secondaryIdentifier()`](https://layrjs.com/docs/v2/reference/component#secondary-identifier-decorator) decorator.\n *\n * A common use case is a `User` component with an immutable primary identifier and a secondary identifier for the email address that can change over time:\n *\n * ```\n * // JS\n *\n * import {Component, primaryIdentifier, secondaryIdentifier} from '﹫layr/component';\n *\n * class User extends Component {\n *   ﹫primaryIdentifier() id;\n *   ﹫secondaryIdentifier() email;\n * }\n * ```\n *\n * ```\n * // TS\n *\n * import {Component, primaryIdentifier, secondaryIdentifier} from '﹫layr/component';\n *\n * class User extends Component {\n *   ﹫primaryIdentifier() id!: string;\n *   ﹫secondaryIdentifier() email!: string;\n * }\n * ```\n *\n * To create a `User` instance, you would do something like:\n *\n * ```\n * const user = new User({email: 'someone@domain.tld'});\n *\n * user.id; // => 'ck41vli1z00013h5xx1esffyn'\n * user.email; // => 'someone@domain.tld'\n * ```\n *\n * Note that the primary identifier (`id`) was auto-generated, but we had to provide a value for the secondary identifier (`email`) because secondary identifiers cannot be `undefined` and they are not commonly auto-generated.\n *\n * Like previously mentioned, contrary to a primary identifier, the value of a secondary identifer can be changed:\n *\n * ```\n * user.email = 'someone-else@domain.tld'; // Okay\n * user.id = 'ck2zrb1xs00013g5to1uimigb'; // Error\n * ```\n *\n * `SecondaryIdentifierAttribute` values are usually of type `'string'` (the default), but you can also have values of type `'number'`:\n *\n * ```\n * // JS\n *\n * import {Component, primaryIdentifier, secondaryIdentifier} from '﹫layr/component';\n *\n * class User extends Component {\n *   ﹫primaryIdentifier() id;\n *   ﹫secondaryIdentifier('number') reference;\n * }\n * ```\n *\n * ```\n * // TS\n *\n * import {Component, primaryIdentifier, secondaryIdentifier} from '﹫layr/component';\n *\n * class User extends Component {\n *   ﹫primaryIdentifier() id!: string;\n *   ﹫secondaryIdentifier('number') reference!: number;\n * }\n * ```\n */\nexport class SecondaryIdentifierAttribute extends IdentifierAttribute {\n  _secondaryIdentifierAttributeBrand!: void;\n\n  /**\n   * Creates an instance of [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute). Typically, instead of using this constructor, you would rather use the [`@secondaryIdentifier()`](https://layrjs.com/docs/v2/reference/component#secondary-identifier-decorator) decorator.\n   *\n   * @param name The name of the attribute.\n   * @param parent The component prototype that owns the attribute.\n   * @param [options.valueType] A string specifying the type of values the attribute can store. It can be either `'string'` or `'number'` (default: `'string'`).\n   * @param [options.default] A function returning the default value of the attribute.\n   * @param [options.validators] An array of [validators](https://layrjs.com/docs/v2/reference/validator) for the value of the attribute.\n   * @param [options.exposure] A [`PropertyExposure`](https://layrjs.com/docs/v2/reference/property#property-exposure-type) object specifying how the attribute should be exposed to remote access.\n   *\n   * @returns The [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute) instance that was created.\n   *\n   * @example\n   * ```\n   * import {Component, SecondaryIdentifierAttribute} from '﹫layr/component';\n   *\n   * class User extends Component {}\n   *\n   * const email = new SecondaryIdentifierAttribute('email', User.prototype);\n   *\n   * email.getName(); // => 'email'\n   * email.getParent(); // => User.prototype\n   * email.getValueType().toString(); // => 'string'\n   * ```\n   *\n   * @category Creation\n   */\n  constructor(name: string, parent: Component, options: AttributeOptions = {}) {\n    super(name, parent, options);\n  }\n\n  // === Property Methods ===\n\n  /**\n   * See the methods that are inherited from the [`Property`](https://layrjs.com/docs/v2/reference/property#basic-methods) class.\n   *\n   * @category Property Methods\n   */\n\n  // === Attribute Methods ===\n\n  /**\n   * See the methods that are inherited from the [`Attribute`](https://layrjs.com/docs/v2/reference/attribute#value-type) class.\n   *\n   * @category Attribute Methods\n   */\n\n  // === Observability ===\n\n  /**\n   * See the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) class.\n   *\n   * @category Observability\n   */\n\n  // === Utilities ===\n\n  static isSecondaryIdentifierAttribute(value: any): value is SecondaryIdentifierAttribute {\n    return isSecondaryIdentifierAttributeInstance(value);\n  }\n}\n\n/**\n * Returns whether the specified value is a `SecondaryIdentifierAttribute` class.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isSecondaryIdentifierAttributeClass(\n  value: any\n): value is typeof SecondaryIdentifierAttribute {\n  return typeof value?.isSecondaryIdentifierAttribute === 'function';\n}\n\n/**\n * Returns whether the specified value is a `SecondaryIdentifierAttribute` instance.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isSecondaryIdentifierAttributeInstance(\n  value: any\n): value is SecondaryIdentifierAttribute {\n  return isSecondaryIdentifierAttributeClass(value?.constructor) === true;\n}\n"
  },
  {
    "path": "packages/component/src/properties/value-types/any-value-type.ts",
    "content": "import {ValueType} from './value-type';\nimport type {Attribute} from '../attribute';\n\nexport class AnyValueType extends ValueType {\n  isOptional() {\n    return true;\n  }\n\n  toString() {\n    return 'any';\n  }\n\n  _checkValue(_value: any, _attribute: Attribute) {\n    return true;\n  }\n\n  static isAnyValueType(value: any): value is AnyValueType {\n    return isAnyValueTypeInstance(value);\n  }\n}\n\nexport function isAnyValueTypeInstance(value: any): value is AnyValueType {\n  return typeof value?.constructor?.isAnyValueType === 'function';\n}\n"
  },
  {
    "path": "packages/component/src/properties/value-types/array-value-type.ts",
    "content": "import {possiblyAsync} from 'possibly-async';\nimport isEmpty from 'lodash/isEmpty';\n\nimport {ValueType, ValueTypeOptions} from './value-type';\nimport type {\n  TraverseAttributesIteratee,\n  TraverseAttributesOptions,\n  ResolveAttributeSelectorOptions\n} from '../../component';\nimport type {Attribute} from '../attribute';\nimport {\n  AttributeSelector,\n  mergeAttributeSelectors,\n  intersectAttributeSelectors\n} from '../attribute-selector';\nimport {SerializeOptions} from '../../serialization';\nimport {joinAttributePath} from '../../utilities';\n\nexport class ArrayValueType extends ValueType {\n  _itemType: ValueType;\n\n  constructor(itemType: ValueType, attribute: Attribute, options: ValueTypeOptions = {}) {\n    super(attribute, options);\n\n    this._itemType = itemType;\n  }\n\n  getItemType() {\n    return this._itemType;\n  }\n\n  toString() {\n    return `${this.getItemType().toString()}[]${super.toString()}`;\n  }\n\n  getScalarType() {\n    return this.getItemType().getScalarType();\n  }\n\n  checkValue(values: unknown[], attribute: Attribute) {\n    super.checkValue(values, attribute);\n\n    if (values === undefined) {\n      // `values` is undefined and isOptional() is true\n      return;\n    }\n\n    const itemType = this.getItemType();\n\n    for (const value of values) {\n      itemType.checkValue(value, attribute);\n    }\n  }\n\n  _checkValue(values: unknown, attribute: Attribute) {\n    return super._checkValue(values, attribute) ?? Array.isArray(values);\n  }\n\n  _traverseAttributes(\n    iteratee: TraverseAttributesIteratee,\n    attribute: Attribute,\n    items: unknown,\n    options: TraverseAttributesOptions\n  ) {\n    const {setAttributesOnly} = options;\n\n    const itemType = this.getItemType();\n\n    if (!setAttributesOnly) {\n      itemType._traverseAttributes(iteratee, attribute, undefined, options);\n      return;\n    }\n\n    if (Array.isArray(items)) {\n      for (const item of items) {\n        itemType._traverseAttributes(iteratee, attribute, item, options);\n      }\n    }\n  }\n\n  _resolveAttributeSelector(\n    normalizedAttributeSelector: AttributeSelector,\n    attribute: Attribute,\n    items: unknown,\n    options: ResolveAttributeSelectorOptions\n  ): AttributeSelector {\n    const {setAttributesOnly, aggregationMode} = options;\n\n    options = {...options, _skipUnchangedAttributes: false, _isArrayItem: true};\n\n    if (normalizedAttributeSelector === false) {\n      return false;\n    }\n\n    const itemType = this.getItemType();\n\n    if (!setAttributesOnly || !Array.isArray(items) || items.length === 0) {\n      return itemType._resolveAttributeSelector(\n        normalizedAttributeSelector,\n        attribute,\n        undefined,\n        options\n      );\n    }\n\n    let resolvedAttributeSelector: AttributeSelector | undefined = undefined;\n\n    const aggregate =\n      aggregationMode === 'union' ? mergeAttributeSelectors : intersectAttributeSelectors;\n\n    for (const item of items) {\n      const itemAttributeSelector = itemType._resolveAttributeSelector(\n        normalizedAttributeSelector,\n        attribute,\n        item,\n        options\n      );\n\n      if (resolvedAttributeSelector === undefined) {\n        resolvedAttributeSelector = itemAttributeSelector;\n      } else {\n        resolvedAttributeSelector = aggregate(resolvedAttributeSelector, itemAttributeSelector);\n      }\n    }\n\n    return resolvedAttributeSelector!;\n  }\n\n  sanitizeValue(values: any[] | undefined) {\n    if (values !== undefined) {\n      const itemType = this.getItemType();\n      values = values.map((value) => itemType.sanitizeValue(value));\n    }\n\n    return super.sanitizeValue(values);\n  }\n\n  runValidators(values: unknown[] | undefined, attributeSelector?: AttributeSelector) {\n    const failedValidators = super.runValidators(values, attributeSelector);\n\n    if (values !== undefined) {\n      const itemType = this.getItemType();\n\n      values.forEach((value, index) => {\n        const failedItemValidators = itemType.runValidators(value, attributeSelector);\n\n        for (const {validator, path} of failedItemValidators) {\n          failedValidators.push({validator, path: joinAttributePath([`[${index}]`, path])});\n        }\n      });\n    }\n\n    return failedValidators;\n  }\n\n  serializeValue(items: unknown, attribute: Attribute, options: SerializeOptions = {}) {\n    if (Array.isArray(items)) {\n      const itemType = this.getItemType();\n\n      return possiblyAsync.map(items, (item) => itemType.serializeValue(item, attribute, options));\n    }\n\n    return super.serializeValue(items, attribute, options);\n  }\n\n  introspect() {\n    const introspectedArrayType = super.introspect();\n\n    const introspectedItemType = this.getItemType().introspect();\n    delete introspectedItemType.valueType;\n\n    if (!isEmpty(introspectedItemType)) {\n      introspectedArrayType.items = introspectedItemType;\n    }\n\n    return introspectedArrayType;\n  }\n\n  static isArrayValueType(value: any): value is ArrayValueType {\n    return isArrayValueTypeInstance(value);\n  }\n}\n\nexport function isArrayValueTypeInstance(value: any): value is ArrayValueType {\n  return typeof value?.constructor?.isArrayValueType === 'function';\n}\n"
  },
  {
    "path": "packages/component/src/properties/value-types/boolean-value-type.ts",
    "content": "import {ValueType} from './value-type';\nimport type {Attribute} from '../attribute';\n\nexport class BooleanValueType extends ValueType {\n  toString() {\n    return `boolean${super.toString()}`;\n  }\n\n  _checkValue(value: unknown, attribute: Attribute) {\n    return super._checkValue(value, attribute) ?? typeof value === 'boolean';\n  }\n\n  static isBooleanValueType(value: any): value is BooleanValueType {\n    return isBooleanValueTypeInstance(value);\n  }\n}\n\nexport function isBooleanValueTypeInstance(value: any): value is BooleanValueType {\n  return typeof value?.constructor?.isBooleanValueType === 'function';\n}\n"
  },
  {
    "path": "packages/component/src/properties/value-types/component-value-type.ts",
    "content": "import {ValueType, ValueTypeOptions} from './value-type';\nimport type {\n  TraverseAttributesIteratee,\n  TraverseAttributesOptions,\n  ResolveAttributeSelectorOptions\n} from '../../component';\nimport type {Attribute} from '../attribute';\nimport type {AttributeSelector} from '../attribute-selector';\nimport {SerializeOptions} from '../../serialization';\nimport {\n  isComponentClassOrInstance,\n  isComponentClass,\n  isComponentInstance,\n  ensureComponentClass,\n  assertIsComponentType\n} from '../../utilities';\n\nexport class ComponentValueType extends ValueType {\n  _componentType: string;\n\n  constructor(componentType: string, attribute: Attribute, options: ValueTypeOptions = {}) {\n    super(attribute, options);\n\n    assertIsComponentType(componentType);\n\n    this._componentType = componentType;\n  }\n\n  getComponentType() {\n    return this._componentType;\n  }\n\n  getComponent(attribute: Attribute) {\n    return ensureComponentClass(attribute.getParent()).getComponentOfType(this.getComponentType());\n  }\n\n  toString() {\n    return `${this.getComponentType()}${super.toString()}`;\n  }\n\n  _checkValue(value: unknown, attribute: Attribute) {\n    const result = super._checkValue(value, attribute);\n\n    if (result !== undefined) {\n      return result;\n    }\n\n    const component = this.getComponent(attribute);\n\n    if (value === component) {\n      return true;\n    }\n\n    if (isComponentClass(value) && isComponentClass(component)) {\n      return value.isForkOf(component);\n    }\n\n    if (isComponentInstance(value) && isComponentInstance(component)) {\n      return value.constructor === component.constructor || value.isForkOf(component);\n    }\n\n    return false;\n  }\n\n  _traverseAttributes(\n    iteratee: TraverseAttributesIteratee,\n    attribute: Attribute,\n    component: unknown,\n    options: TraverseAttributesOptions\n  ) {\n    const {setAttributesOnly} = options;\n\n    if (!setAttributesOnly) {\n      component = this.getComponent(attribute);\n    }\n\n    if (isComponentClassOrInstance(component)) {\n      component.__traverseAttributes(iteratee, options);\n    }\n  }\n\n  _resolveAttributeSelector(\n    normalizedAttributeSelector: AttributeSelector,\n    attribute: Attribute,\n    component: unknown,\n    options: ResolveAttributeSelectorOptions\n  ): AttributeSelector {\n    const {setAttributesOnly} = options;\n\n    if (normalizedAttributeSelector === false) {\n      return false;\n    }\n\n    if (!setAttributesOnly) {\n      component = this.getComponent(attribute);\n    }\n\n    if (!isComponentClassOrInstance(component)) {\n      return {}; // `setAttributesOnly` is true and `component` is undefined\n    }\n\n    return component.__resolveAttributeSelector(normalizedAttributeSelector, options);\n  }\n\n  runValidators(value: unknown, attributeSelector?: AttributeSelector) {\n    const failedValidators = super.runValidators(value, attributeSelector);\n\n    if (isComponentClassOrInstance(value)) {\n      const componentFailedValidators = value.runValidators(attributeSelector);\n      failedValidators.push(...componentFailedValidators);\n    }\n\n    return failedValidators;\n  }\n\n  serializeValue(value: unknown, attribute: Attribute, options: SerializeOptions = {}) {\n    if (isComponentClassOrInstance(value)) {\n      return value.__serialize(options);\n    }\n\n    return super.serializeValue(value, attribute, options);\n  }\n\n  canDeserializeInPlace(attribute: Attribute) {\n    return ensureComponentClass(this.getComponent(attribute)).isEmbedded();\n  }\n\n  static isComponentValueType(value: any): value is ComponentValueType {\n    return isComponentValueTypeInstance(value);\n  }\n}\n\nexport function isComponentValueTypeInstance(value: any): value is ComponentValueType {\n  return typeof value?.constructor?.isComponentValueType === 'function';\n}\n"
  },
  {
    "path": "packages/component/src/properties/value-types/date-value-type.ts",
    "content": "import {ValueType} from './value-type';\nimport type {Attribute} from '../attribute';\n\nexport class DateValueType extends ValueType {\n  toString() {\n    return `Date${super.toString()}`;\n  }\n\n  _checkValue(value: unknown, attribute: Attribute) {\n    return super._checkValue(value, attribute) ?? value instanceof Date;\n  }\n\n  static isDateValueType(value: any): value is DateValueType {\n    return isDateValueTypeInstance(value);\n  }\n}\n\nexport function isDateValueTypeInstance(value: any): value is DateValueType {\n  return typeof value?.constructor?.isDateValueType === 'function';\n}\n"
  },
  {
    "path": "packages/component/src/properties/value-types/factory.test.ts",
    "content": "import {Component} from '../../component';\nimport {Attribute} from '../attribute';\nimport {createValueType} from './factory';\nimport {sanitizers} from '../../sanitization';\nimport {validators} from '../../validation';\nimport {isAnyValueTypeInstance} from './any-value-type';\nimport {isNumberValueTypeInstance} from './number-value-type';\nimport {isStringValueTypeInstance} from './string-value-type';\nimport {ArrayValueType, isArrayValueTypeInstance} from './array-value-type';\n\ndescribe('Factory', () => {\n  class TestComponent extends Component {}\n\n  const attribute = new Attribute('testAttribute', TestComponent.prototype);\n\n  test('createValueType()', async () => {\n    let type = createValueType('string', attribute);\n\n    expect(isStringValueTypeInstance(type)).toBe(true);\n    expect(type.isOptional()).toBe(false);\n\n    type = createValueType('string?', attribute);\n\n    expect(isStringValueTypeInstance(type)).toBe(true);\n    expect(type.isOptional()).toBe(true);\n\n    type = createValueType('number[]', attribute);\n\n    expect(isArrayValueTypeInstance(type)).toBe(true);\n    expect(type.isOptional()).toBe(false);\n    expect(isNumberValueTypeInstance((type as ArrayValueType).getItemType())).toBe(true);\n    expect((type as ArrayValueType).getItemType().isOptional()).toBe(false);\n\n    type = createValueType('number?[]?', attribute);\n\n    expect(isArrayValueTypeInstance(type)).toBe(true);\n    expect(type.isOptional()).toBe(true);\n    expect(isNumberValueTypeInstance((type as ArrayValueType).getItemType())).toBe(true);\n    expect((type as ArrayValueType).getItemType().isOptional()).toBe(true);\n\n    type = createValueType('string', attribute);\n\n    expect(type.getSanitizers()).toEqual([]);\n    expect(type.getValidators()).toEqual([]);\n\n    const trim = sanitizers.trim();\n    const notEmpty = validators.notEmpty();\n    type = createValueType('string', attribute, {sanitizers: [trim], validators: [notEmpty]});\n\n    expect(type.getSanitizers()).toEqual([trim]);\n    expect(type.getValidators()).toEqual([notEmpty]);\n\n    const compact = sanitizers.compact();\n    type = createValueType('string[]', attribute, {\n      sanitizers: [compact],\n      validators: [notEmpty],\n      items: {sanitizers: [trim], validators: [notEmpty]}\n    });\n\n    expect(type.getSanitizers()).toEqual([compact]);\n    expect(type.getValidators()).toEqual([notEmpty]);\n    expect((type as ArrayValueType).getItemType().getSanitizers()).toEqual([trim]);\n    expect((type as ArrayValueType).getItemType().getValidators()).toEqual([notEmpty]);\n\n    type = createValueType('string[][]', attribute, {\n      items: {items: {sanitizers: [trim], validators: [notEmpty]}}\n    });\n\n    expect(\n      ((type as ArrayValueType).getItemType() as ArrayValueType).getItemType().getSanitizers()\n    ).toEqual([trim]);\n    expect(\n      ((type as ArrayValueType).getItemType() as ArrayValueType).getItemType().getValidators()\n    ).toEqual([notEmpty]);\n\n    type = createValueType(undefined, attribute);\n\n    expect(isAnyValueTypeInstance(type)).toBe(true);\n    expect(type.isOptional()).toBe(true);\n\n    type = createValueType('', attribute);\n\n    expect(isAnyValueTypeInstance(type)).toBe(true);\n    expect(type.isOptional()).toBe(true);\n\n    type = createValueType('?', attribute);\n\n    expect(isAnyValueTypeInstance(type)).toBe(true);\n    expect(type.isOptional()).toBe(true);\n\n    type = createValueType('[]', attribute);\n\n    expect(isArrayValueTypeInstance(type)).toBe(true);\n    expect(type.isOptional()).toBe(false);\n    expect(isAnyValueTypeInstance((type as ArrayValueType).getItemType())).toBe(true);\n    expect((type as ArrayValueType).getItemType().isOptional()).toBe(true);\n\n    expect(() => createValueType('date', attribute)).toThrow(\n      \"The specified type is invalid (attribute: 'TestComponent.prototype.testAttribute', type: 'date')\"\n    );\n    expect(() => createValueType('movie', attribute)).toThrow(\n      \"The specified type is invalid (attribute: 'TestComponent.prototype.testAttribute', type: 'movie')\"\n    );\n    expect(() => createValueType('date?', attribute)).toThrow(\n      \"The specified type is invalid (attribute: 'TestComponent.prototype.testAttribute', type: 'date?')\"\n    );\n    expect(() => createValueType('[movie]', attribute)).toThrow(\n      \"The specified type is invalid (attribute: 'TestComponent.prototype.testAttribute', type: '[movie]')\"\n    );\n  });\n\n  test('Sanitization', async () => {\n    const trim = sanitizers.trim();\n    const compact = sanitizers.compact();\n\n    let type = createValueType('string', attribute, {sanitizers: [trim]});\n\n    expect(type.sanitizeValue('hello')).toBe('hello');\n    expect(type.sanitizeValue(' hello ')).toBe('hello');\n    expect(type.sanitizeValue(undefined)).toBe(undefined);\n\n    type = createValueType('string[]', attribute, {\n      sanitizers: [compact],\n      items: {sanitizers: [trim]}\n    });\n\n    expect(type.sanitizeValue(['hello'])).toStrictEqual(['hello']);\n    expect(type.sanitizeValue(['hello', ' '])).toStrictEqual(['hello']);\n    expect(type.sanitizeValue([' '])).toStrictEqual([]);\n    expect(type.sanitizeValue(undefined)).toBe(undefined);\n  });\n\n  test('Validation', async () => {\n    const notEmpty = validators.notEmpty();\n\n    let type = createValueType('string', attribute, {validators: [notEmpty]});\n\n    expect(type.runValidators('Inception')).toEqual([]);\n    expect(type.runValidators('')).toEqual([{validator: notEmpty, path: ''}]);\n    expect(type.runValidators(undefined)).toEqual([{validator: notEmpty, path: ''}]);\n\n    type = createValueType('string[]', attribute, {\n      validators: [notEmpty],\n      items: {validators: [notEmpty]}\n    });\n\n    expect(type.runValidators(['Inception'])).toEqual([]);\n    expect(type.runValidators([])).toEqual([{validator: notEmpty, path: ''}]);\n    expect(type.runValidators(undefined)).toEqual([{validator: notEmpty, path: ''}]);\n    expect(type.runValidators(['Inception', ''])).toEqual([{validator: notEmpty, path: '[1]'}]);\n    expect(type.runValidators(['Inception', undefined])).toEqual([\n      {validator: notEmpty, path: '[1]'}\n    ]);\n\n    type = createValueType('string[][]', attribute, {items: {items: {validators: [notEmpty]}}});\n\n    expect(type.runValidators([['Inception', '']])).toEqual([\n      {validator: notEmpty, path: '[0][1]'}\n    ]);\n  });\n});\n"
  },
  {
    "path": "packages/component/src/properties/value-types/factory.ts",
    "content": "import type {Attribute} from '../attribute';\nimport type {ValueType, IntrospectedValueType} from './value-type';\nimport {AnyValueType} from './any-value-type';\nimport {BooleanValueType} from './boolean-value-type';\nimport {NumberValueType} from './number-value-type';\nimport {StringValueType} from './string-value-type';\nimport {ObjectValueType} from './object-value-type';\nimport {DateValueType} from './date-value-type';\nimport {RegExpValueType} from './regexp-value-type';\nimport {ArrayValueType} from './array-value-type';\nimport {ComponentValueType} from './component-value-type';\nimport {Sanitizer, SanitizerFunction} from '../../sanitization';\nimport {Validator, ValidatorFunction} from '../../validation';\nimport {isComponentType} from '../../utilities';\n\nconst VALUE_TYPE_MAP = new Map(\n  Object.entries({\n    any: AnyValueType,\n    boolean: BooleanValueType,\n    number: NumberValueType,\n    string: StringValueType,\n    object: ObjectValueType,\n    Date: DateValueType,\n    RegExp: RegExpValueType\n  })\n);\n\nexport type UnintrospectedValueType = {\n  valueType?: string;\n  validators?: Validator[];\n  items?: UnintrospectedValueType;\n};\n\ntype CreateValueTypeOptions = {\n  sanitizers?: (Sanitizer | SanitizerFunction)[];\n  validators?: (Validator | ValidatorFunction)[];\n  items?: CreateValueTypeOptions;\n};\n\nexport function createValueType(\n  specifier: string | undefined,\n  attribute: Attribute,\n  options: CreateValueTypeOptions = {}\n): ValueType {\n  const {sanitizers, validators = [], items} = options;\n\n  let type = specifier ? specifier : 'any';\n  let isOptional: boolean;\n\n  if (type.endsWith('?')) {\n    isOptional = true;\n    type = type.slice(0, -1);\n\n    if (type === '') {\n      type = 'any';\n    }\n  } else {\n    isOptional = false;\n  }\n\n  if (type.endsWith('[]')) {\n    const itemSpecifier = type.slice(0, -2);\n    const itemType = createValueType(itemSpecifier, attribute, {...items});\n    return new ArrayValueType(itemType, attribute, {isOptional, sanitizers, validators});\n  }\n\n  if (items !== undefined) {\n    throw new Error(\n      `The 'items' option cannot be specified for a type that is not an array (${attribute.describe()}, type: '${specifier}')`\n    );\n  }\n\n  const ValueTypeClass = VALUE_TYPE_MAP.get(type);\n\n  if (ValueTypeClass !== undefined) {\n    return new ValueTypeClass(attribute, {isOptional, sanitizers, validators});\n  }\n\n  if (!isComponentType(type)) {\n    throw new Error(\n      `The specified type is invalid (${attribute.describe()}, type: '${specifier}')`\n    );\n  }\n\n  return new ComponentValueType(type, attribute, {isOptional, sanitizers, validators});\n}\n\nexport function unintrospectValueType({\n  valueType,\n  validators,\n  items: introspectedItems\n}: IntrospectedValueType) {\n  let unintrospectedItems: UnintrospectedValueType | undefined;\n\n  if (introspectedItems !== undefined) {\n    unintrospectedItems = unintrospectValueType(introspectedItems);\n  }\n\n  const unintrospectedValueType: UnintrospectedValueType = {};\n\n  if (valueType !== undefined) {\n    unintrospectedValueType.valueType = valueType;\n  }\n\n  if (validators !== undefined) {\n    unintrospectedValueType.validators = validators;\n  }\n\n  if (unintrospectedItems !== undefined) {\n    unintrospectedValueType.items = unintrospectedItems;\n  }\n\n  return unintrospectedValueType;\n}\n"
  },
  {
    "path": "packages/component/src/properties/value-types/index.ts",
    "content": "export * from './any-value-type';\nexport * from './array-value-type';\nexport * from './boolean-value-type';\nexport * from './component-value-type';\nexport * from './date-value-type';\nexport * from './factory';\nexport * from './number-value-type';\nexport * from './object-value-type';\nexport * from './regexp-value-type';\nexport * from './string-value-type';\nexport * from './value-type';\n"
  },
  {
    "path": "packages/component/src/properties/value-types/number-value-type.ts",
    "content": "import {ValueType} from './value-type';\nimport type {Attribute} from '../attribute';\n\nexport class NumberValueType extends ValueType {\n  toString() {\n    return `number${super.toString()}`;\n  }\n\n  _checkValue(value: unknown, attribute: Attribute) {\n    return super._checkValue(value, attribute) ?? typeof value === 'number';\n  }\n\n  static isNumberValueType(value: any): value is NumberValueType {\n    return isNumberValueTypeInstance(value);\n  }\n}\n\nexport function isNumberValueTypeInstance(value: any): value is NumberValueType {\n  return typeof value?.constructor?.isNumberValueType === 'function';\n}\n"
  },
  {
    "path": "packages/component/src/properties/value-types/object-value-type.ts",
    "content": "import isPlainObject from 'lodash/isPlainObject';\n\nimport {ValueType} from './value-type';\nimport type {Attribute} from '../attribute';\n\nexport class ObjectValueType extends ValueType {\n  toString() {\n    return `object${super.toString()}`;\n  }\n\n  _checkValue(value: unknown, attribute: Attribute) {\n    return super._checkValue(value, attribute) ?? isPlainObject(value);\n  }\n\n  static isObjectValueType(value: any): value is ObjectValueType {\n    return isObjectValueTypeInstance(value);\n  }\n}\n\nexport function isObjectValueTypeInstance(value: any): value is ObjectValueType {\n  return typeof value?.constructor?.isObjectValueType === 'function';\n}\n"
  },
  {
    "path": "packages/component/src/properties/value-types/regexp-value-type.ts",
    "content": "import {ValueType} from './value-type';\nimport type {Attribute} from '../attribute';\n\nexport class RegExpValueType extends ValueType {\n  toString() {\n    return `RegExp${super.toString()}`;\n  }\n\n  _checkValue(value: unknown, attribute: Attribute) {\n    return super._checkValue(value, attribute) ?? value instanceof RegExp;\n  }\n\n  static isRegExpValueType(value: any): value is RegExpValueType {\n    return isRegExpValueTypeInstance(value);\n  }\n}\n\nexport function isRegExpValueTypeInstance(value: any): value is RegExpValueType {\n  return typeof value?.constructor?.isRegExpValueType === 'function';\n}\n"
  },
  {
    "path": "packages/component/src/properties/value-types/string-value-type.ts",
    "content": "import {ValueType} from './value-type';\nimport type {Attribute} from '../attribute';\n\nexport class StringValueType extends ValueType {\n  toString() {\n    return `string${super.toString()}`;\n  }\n\n  _checkValue(value: unknown, attribute: Attribute) {\n    return super._checkValue(value, attribute) ?? typeof value === 'string';\n  }\n\n  static isStringValueType(value: any): value is StringValueType {\n    return isStringValueTypeInstance(value);\n  }\n}\n\nexport function isStringValueTypeInstance(value: any): value is StringValueType {\n  return typeof value?.constructor?.isStringValueType === 'function';\n}\n"
  },
  {
    "path": "packages/component/src/properties/value-types/value-type.test.ts",
    "content": "import {Component} from '../../component';\nimport {Attribute} from '../attribute';\nimport {provide} from '../../decorators';\nimport {AnyValueType, isAnyValueTypeInstance} from './any-value-type';\nimport {BooleanValueType, isBooleanValueTypeInstance} from './boolean-value-type';\nimport {NumberValueType, isNumberValueTypeInstance} from './number-value-type';\nimport {StringValueType, isStringValueTypeInstance} from './string-value-type';\nimport {ObjectValueType, isObjectValueTypeInstance} from './object-value-type';\nimport {DateValueType, isDateValueTypeInstance} from './date-value-type';\nimport {RegExpValueType, isRegExpValueTypeInstance} from './regexp-value-type';\nimport {ArrayValueType, isArrayValueTypeInstance} from './array-value-type';\nimport {ComponentValueType, isComponentValueTypeInstance} from './component-value-type';\n\ndescribe('ValueType', () => {\n  class TestComponent extends Component {}\n\n  const attribute = new Attribute('testAttribute', TestComponent.prototype);\n\n  test('AnyValueType', async () => {\n    let type = new AnyValueType(attribute);\n\n    expect(isAnyValueTypeInstance(type)).toBe(true);\n\n    expect(type.toString()).toBe('any');\n\n    expect(() => type.checkValue(true, attribute)).not.toThrow();\n    expect(() => type.checkValue(1, attribute)).not.toThrow();\n    expect(() => type.checkValue('a', attribute)).not.toThrow();\n    expect(() => type.checkValue({}, attribute)).not.toThrow();\n    expect(() => type.checkValue(undefined, attribute)).not.toThrow();\n\n    // The 'isOptional' option should no effects for the 'any' type\n    type = new AnyValueType(attribute, {isOptional: true});\n\n    expect(type.toString()).toBe('any');\n\n    expect(() => type.checkValue(true, attribute)).not.toThrow();\n    expect(() => type.checkValue(1, attribute)).not.toThrow();\n    expect(() => type.checkValue('a', attribute)).not.toThrow();\n    expect(() => type.checkValue({}, attribute)).not.toThrow();\n    expect(() => type.checkValue(undefined, attribute)).not.toThrow();\n  });\n\n  test('BooleanValueType', async () => {\n    let type = new BooleanValueType(attribute);\n\n    expect(isBooleanValueTypeInstance(type)).toBe(true);\n\n    expect(type.toString()).toBe('boolean');\n\n    expect(() => type.checkValue(true, attribute)).not.toThrow();\n    expect(() => type.checkValue(false, attribute)).not.toThrow();\n    expect(() => type.checkValue(1, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'boolean', received type: 'number')\"\n    );\n    expect(() => type.checkValue(undefined, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'boolean', received type: 'undefined')\"\n    );\n\n    type = new BooleanValueType(attribute, {isOptional: true});\n\n    expect(type.toString()).toBe('boolean?');\n\n    expect(() => type.checkValue(true, attribute)).not.toThrow();\n    expect(() => type.checkValue(false, attribute)).not.toThrow();\n    expect(() => type.checkValue(1, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'boolean?', received type: 'number')\"\n    );\n    expect(() => type.checkValue(undefined, attribute)).not.toThrow();\n  });\n\n  test('NumberValueType', async () => {\n    let type = new NumberValueType(attribute);\n\n    expect(isNumberValueTypeInstance(type)).toBe(true);\n\n    expect(type.toString()).toBe('number');\n\n    expect(() => type.checkValue(1, attribute)).not.toThrow();\n    expect(() => type.checkValue('a', attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'number', received type: 'string')\"\n    );\n    expect(() => type.checkValue(undefined, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'number', received type: 'undefined')\"\n    );\n\n    type = new NumberValueType(attribute, {isOptional: true});\n\n    expect(type.toString()).toBe('number?');\n\n    expect(() => type.checkValue(1, attribute)).not.toThrow();\n    expect(() => type.checkValue('a', attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'number?', received type: 'string')\"\n    );\n    expect(() => type.checkValue(undefined, attribute)).not.toThrow();\n  });\n\n  test('StringValueType', async () => {\n    let type = new StringValueType(attribute);\n\n    expect(isStringValueTypeInstance(type)).toBe(true);\n\n    expect(type.toString()).toBe('string');\n\n    expect(() => type.checkValue('a', attribute)).not.toThrow();\n    expect(() => type.checkValue(1, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'string', received type: 'number')\"\n    );\n    expect(() => type.checkValue(undefined, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'string', received type: 'undefined')\"\n    );\n\n    type = new StringValueType(attribute, {isOptional: true});\n\n    expect(type.toString()).toBe('string?');\n\n    expect(() => type.checkValue('a', attribute)).not.toThrow();\n    expect(() => type.checkValue(1, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'string?', received type: 'number')\"\n    );\n    expect(() => type.checkValue(undefined, attribute)).not.toThrow();\n  });\n\n  test('ObjectValueType', async () => {\n    let type = new ObjectValueType(attribute);\n\n    expect(isObjectValueTypeInstance(type)).toBe(true);\n\n    class Movie extends Component {}\n\n    const movie = new Movie();\n\n    expect(type.toString()).toBe('object');\n\n    expect(() => type.checkValue({}, attribute)).not.toThrow();\n    expect(() => type.checkValue({title: 'Inception'}, attribute)).not.toThrow();\n    expect(() => type.checkValue('a', attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'object', received type: 'string')\"\n    );\n    expect(() => type.checkValue(movie, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'object', received type: 'Movie')\"\n    );\n    expect(() => type.checkValue(undefined, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'object', received type: 'undefined')\"\n    );\n\n    type = new ObjectValueType(attribute, {isOptional: true});\n\n    expect(type.toString()).toBe('object?');\n\n    expect(() => type.checkValue({}, attribute)).not.toThrow();\n    expect(() => type.checkValue({title: 'Inception'}, attribute)).not.toThrow();\n    expect(() => type.checkValue(movie, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'object?', received type: 'Movie')\"\n    );\n    expect(() => type.checkValue('a', attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'object?', received type: 'string')\"\n    );\n    expect(() => type.checkValue(undefined, attribute)).not.toThrow();\n  });\n\n  test('DateValueType', async () => {\n    let type = new DateValueType(attribute);\n\n    expect(isDateValueTypeInstance(type)).toBe(true);\n\n    expect(type.toString()).toBe('Date');\n\n    expect(() => type.checkValue(new Date(), attribute)).not.toThrow();\n    expect(() => type.checkValue('a', attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'Date', received type: 'string')\"\n    );\n    expect(() => type.checkValue(undefined, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'Date', received type: 'undefined')\"\n    );\n\n    type = new DateValueType(attribute, {isOptional: true});\n\n    expect(type.toString()).toBe('Date?');\n\n    expect(() => type.checkValue(new Date(), attribute)).not.toThrow();\n    expect(() => type.checkValue('a', attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'Date?', received type: 'string')\"\n    );\n    expect(() => type.checkValue(undefined, attribute)).not.toThrow();\n  });\n\n  test('RegExpValueType', async () => {\n    let type = new RegExpValueType(attribute);\n\n    expect(isRegExpValueTypeInstance(type)).toBe(true);\n\n    expect(type.toString()).toBe('RegExp');\n\n    expect(() => type.checkValue(/abc/, attribute)).not.toThrow();\n    expect(() => type.checkValue('a', attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'RegExp', received type: 'string')\"\n    );\n    expect(() => type.checkValue(undefined, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'RegExp', received type: 'undefined')\"\n    );\n\n    type = new RegExpValueType(attribute, {isOptional: true});\n\n    expect(type.toString()).toBe('RegExp?');\n\n    expect(() => type.checkValue(/abc/, attribute)).not.toThrow();\n    expect(() => type.checkValue('a', attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'RegExp?', received type: 'string')\"\n    );\n    expect(() => type.checkValue(undefined, attribute)).not.toThrow();\n  });\n\n  test('ArrayValueType', async () => {\n    let itemType = new NumberValueType(attribute);\n    let type = new ArrayValueType(itemType, attribute);\n\n    expect(isArrayValueTypeInstance(type)).toBe(true);\n\n    expect(type.toString()).toBe('number[]');\n\n    expect(() => type.checkValue([], attribute)).not.toThrow();\n    expect(() => type.checkValue([1], attribute)).not.toThrow();\n    // @ts-expect-error\n    expect(() => type.checkValue(1, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'number[]', received type: 'number')\"\n    );\n    expect(() => type.checkValue(['a'], attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'number', received type: 'string')\"\n    );\n    // @ts-expect-error\n    expect(() => type.checkValue(undefined, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'number[]', received type: 'undefined')\"\n    );\n    expect(() => type.checkValue([undefined], attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'number', received type: 'undefined')\"\n    );\n    expect(() => type.checkValue([1, undefined], attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'number', received type: 'undefined')\"\n    );\n    expect(() => type.checkValue([undefined, 1], attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'number', received type: 'undefined')\"\n    );\n\n    itemType = new NumberValueType(attribute, {isOptional: true});\n    type = new ArrayValueType(itemType, attribute, {isOptional: true});\n\n    expect(type.toString()).toBe('number?[]?');\n\n    expect(() => type.checkValue([], attribute)).not.toThrow();\n    expect(() => type.checkValue([1], attribute)).not.toThrow();\n    // @ts-expect-error\n    expect(() => type.checkValue(1, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'number?[]?', received type: 'number')\"\n    );\n    expect(() => type.checkValue(['a'], attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'TestComponent.prototype.testAttribute', expected type: 'number?', received type: 'string')\"\n    );\n    // @ts-expect-error\n    expect(() => type.checkValue(undefined, attribute)).not.toThrow();\n    expect(() => type.checkValue([undefined], attribute)).not.toThrow();\n    expect(() => type.checkValue([1, undefined], attribute)).not.toThrow();\n    expect(() => type.checkValue([undefined, 1], attribute)).not.toThrow();\n  });\n\n  test('ComponentValueType', async () => {\n    class Movie extends Component {}\n\n    class Actor extends Component {}\n\n    class App extends Component {\n      @provide() static Movie = Movie;\n      @provide() static Actor = Actor;\n    }\n\n    const attribute = new Attribute('testAttribute', App);\n\n    const movie = new Movie();\n    const actor = new Actor();\n\n    // Component class value types\n\n    let type = new ComponentValueType('typeof Movie', attribute);\n\n    expect(isComponentValueTypeInstance(type)).toBe(true);\n\n    expect(type.toString()).toBe('typeof Movie');\n\n    expect(() => type.checkValue(Movie, attribute)).not.toThrow();\n    expect(() => type.checkValue(Actor, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'App.testAttribute', expected type: 'typeof Movie', received type: 'typeof Actor')\"\n    );\n    expect(() => type.checkValue(movie, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'App.testAttribute', expected type: 'typeof Movie', received type: 'Movie')\"\n    );\n    expect(() => type.checkValue({}, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'App.testAttribute', expected type: 'typeof Movie', received type: 'object')\"\n    );\n    expect(() => type.checkValue(undefined, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'App.testAttribute', expected type: 'typeof Movie', received type: 'undefined')\"\n    );\n\n    type = new ComponentValueType('typeof Movie', attribute, {isOptional: true});\n\n    expect(type.toString()).toBe('typeof Movie?');\n\n    expect(() => type.checkValue(Movie, attribute)).not.toThrow();\n    expect(() => type.checkValue(Actor, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'App.testAttribute', expected type: 'typeof Movie?', received type: 'typeof Actor')\"\n    );\n    expect(() => type.checkValue(movie, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'App.testAttribute', expected type: 'typeof Movie?', received type: 'Movie')\"\n    );\n    expect(() => type.checkValue({}, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'App.testAttribute', expected type: 'typeof Movie?', received type: 'object')\"\n    );\n    expect(() => type.checkValue(undefined, attribute)).not.toThrow();\n\n    // Component instance value types\n\n    type = new ComponentValueType('Movie', attribute);\n\n    expect(isComponentValueTypeInstance(type)).toBe(true);\n\n    expect(type.toString()).toBe('Movie');\n\n    expect(() => type.checkValue(movie, attribute)).not.toThrow();\n    expect(() => type.checkValue(actor, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'App.testAttribute', expected type: 'Movie', received type: 'Actor')\"\n    );\n    expect(() => type.checkValue(Movie, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'App.testAttribute', expected type: 'Movie', received type: 'typeof Movie')\"\n    );\n    expect(() => type.checkValue({}, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'App.testAttribute', expected type: 'Movie', received type: 'object')\"\n    );\n    expect(() => type.checkValue(undefined, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'App.testAttribute', expected type: 'Movie', received type: 'undefined')\"\n    );\n\n    type = new ComponentValueType('Movie', attribute, {isOptional: true});\n\n    expect(type.toString()).toBe('Movie?');\n\n    expect(() => type.checkValue(movie, attribute)).not.toThrow();\n    expect(() => type.checkValue(actor, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'App.testAttribute', expected type: 'Movie?', received type: 'Actor')\"\n    );\n    expect(() => type.checkValue(Movie, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'App.testAttribute', expected type: 'Movie?', received type: 'typeof Movie')\"\n    );\n    expect(() => type.checkValue({}, attribute)).toThrow(\n      \"Cannot assign a value of an unexpected type (attribute: 'App.testAttribute', expected type: 'Movie?', received type: 'object')\"\n    );\n    expect(() => type.checkValue(undefined, attribute)).not.toThrow();\n  });\n});\n"
  },
  {
    "path": "packages/component/src/properties/value-types/value-type.ts",
    "content": "import {getTypeOf} from 'core-helpers';\n\nimport type {\n  TraverseAttributesIteratee,\n  TraverseAttributesOptions,\n  ResolveAttributeSelectorOptions\n} from '../../component';\nimport type {Attribute} from '../attribute';\nimport type {AttributeSelector} from '../attribute-selector';\nimport {Sanitizer, SanitizerFunction, runSanitizers, normalizeSanitizer} from '../../sanitization';\nimport {Validator, ValidatorFunction, runValidators, normalizeValidator} from '../../validation';\nimport {serialize, SerializeOptions} from '../../serialization';\n\nexport type IntrospectedValueType = {\n  valueType?: string;\n  validators?: Validator[];\n  items?: IntrospectedValueType;\n};\n\nexport type ValueTypeOptions = {\n  isOptional?: boolean;\n  sanitizers?: (Sanitizer | SanitizerFunction)[];\n  validators?: (Validator | ValidatorFunction)[];\n};\n\n/**\n * A class to handle the various types of values supported by Layr.\n *\n * #### Usage\n *\n * You shouldn't have to create a `ValueType` instance directly. Instead, when you define an attribute (using a decorator such as [`@attribute()`](https://layrjs.com/docs/v2/reference/component#attribute-decorator)), you can specify a string representing a type of value, and a `ValueType` will be automatically created for you.\n *\n * **Example:**\n *\n * ```\n * // JS\n *\n * import {Component, attribute, validators} from '﹫layr/component';\n *\n * const {integer, greaterThan} = validators;\n *\n * class Movie extends Component {\n *   // Required 'string' attribute\n *   ﹫attribute('string') title;\n *\n *   // Required 'number' attribute with some validators\n *   ﹫attribute('number', {validators: [integer(), greaterThan(0)]}) reference;\n *\n *   // Optional 'string' attribute\n *   ﹫attribute('string?') summary;\n *\n *   // Required 'Director' attribute\n *   ﹫attribute('Director') director;\n *\n *   // Required array of 'Actor' attribute with a default value\n *   ﹫attribute('Actor[]') actors = [];\n * }\n * ```\n *\n * ```\n * // TS\n *\n * import {Component, attribute, validators} from '﹫layr/component';\n *\n * const {integer, greaterThan} = validators;\n *\n * class Movie extends Component {\n *   // Required 'string' attribute\n *   ﹫attribute('string') title!: string;\n *\n *   // Required 'number' attribute with some validators\n *   ﹫attribute('number', {validators: [integer(), greaterThan(0)]}) reference!: number;\n *\n *   // Optional 'string' attribute\n *   ﹫attribute('string?') summary?: string;\n *\n *   // Required 'Director' attribute\n *   ﹫attribute('Director') director!: Director;\n *\n *   // Required array of 'Actor' attribute with a default value\n *   ﹫attribute('Actor[]') actors: Actor[] = [];\n * }\n * ```\n *\n * In case you want to access the `ValueType` instances that were created under the hood, you can do the following:\n *\n * ```\n * const movie = new Movie({ ... });\n *\n * let valueType = movie.getAttribute('title').getValueType();\n * valueType.toString(); // => 'string'\n *\n * valueType = movie.getAttribute('reference').getValueType();\n * valueType.toString(); // => 'number'\n * valueType.getValidators(); // => [integerValidator, greaterThanValidator]\n *\n * valueType = movie.getAttribute('summary').getValueType();\n * valueType.toString(); // => 'string?'\n * valueType.isOptional(); // => true\n *\n * valueType = movie.getAttribute('director').getValueType();\n * valueType.toString(); // => 'Director'\n *\n * valueType = movie.getAttribute('actors').getValueType();\n * valueType.toString(); // => 'Actor[]'\n * const itemValueType = valueType.getItemType(); // => A ValueType representing the type of the items inside the array\n * itemValueType.toString(); // => Actor\n * ```\n *\n * #### Supported Types\n *\n * Layr supports a number of types that can be represented by a string in a way that is very similar to the way you specify basic types in [TypeScript](https://www.typescriptlang.org/).\n *\n * ##### Scalars\n *\n * To specify a scalar type, simply specify a string representing it:\n *\n * * `'boolean'`: A boolean.\n * * `'number'`: A floating-point number.\n * * `'string'`: A string.\n *\n * ##### Arrays\n *\n * To specify an array type, add `'[]'` after any other type:\n *\n * * `'number[]'`: An array of numbers.\n * * `'string[]'`: An array of strings.\n * * `'Actor[]'`: An array of `Actor`.\n * * `'number[][]'`: A matrix of numbers.\n *\n * ##### Objects\n *\n * To specify a plain object type, just specify the string `'object'`:\n *\n * * `'object'`: A plain JavaScript object.\n *\n * Some common JavaScript objects are supported as well:\n *\n * * `'Date'`: A JavaScript `Date` instance.\n * * `'RegExp'`: A JavaScript `RegExp` instance.\n *\n * ##### Components\n *\n * An attribute can hold a reference to a [`Component`](https://layrjs.com/docs/v2/reference/component) instance, or contain an [`EmbeddedComponent`](https://layrjs.com/docs/v2/reference/embedded-component) instance. To specify such a type, just specify the name of the component:\n *\n * * `'Director'`: A reference to a `Director` component instance.\n * * `'MovieDetails'`: A `MovieDetails` embedded component instance.\n *\n * It is also possible to specify a type that represents a reference to a [`Component`](https://layrjs.com/docs/v2/reference/component) class. To do so, add `'typeof '` before the name of the component:\n *\n * * `'typeof Director'`: A reference to the `Director` component class.\n *\n * ##### `'?'` Modifier\n *\n * By default, all attribute values are required, which means a value cannot be `undefined`. To make a value optional, add a question mark (`'?'`) after its type:\n *\n * * `'string?'`: A string or `undefined`.\n * * `'number[]?'`: A number array or `undefined`.\n * * `'number?[]'`: An array containing some values of type number or `undefined`.\n * * `'Director?'`: A reference to a `Director` component instance or `undefined`.\n *\n * ##### '`any`' Type\n *\n * In some rare occasions, you may want to define an attribute that can handle any type of values. To do so, you can specify the string `'any'`:\n *\n * * `'any'`: Any type of values.\n */\nexport class ValueType {\n  _isOptional: boolean | undefined;\n  _sanitizers: Sanitizer[];\n  _validators: Validator[];\n\n  constructor(attribute: Attribute, options: ValueTypeOptions = {}) {\n    const {isOptional, sanitizers = [], validators = []} = options;\n\n    const normalizedSanitizers = sanitizers.map((sanitizer) =>\n      normalizeSanitizer(sanitizer, attribute)\n    );\n\n    const normalizedValidators = validators.map((validator) =>\n      normalizeValidator(validator, attribute)\n    );\n\n    this._isOptional = isOptional;\n    this._sanitizers = normalizedSanitizers;\n    this._validators = normalizedValidators;\n  }\n\n  /**\n   * Returns whether the value type is marked as optional. A value of a type marked as optional can be `undefined`.\n   *\n   * @returns A boolean.\n   *\n   * @example\n   * ```\n   * movie.getAttribute('summary').getValueType().isOptional(); // => true\n   * movie.summary = undefined; // Okay\n   *\n   * movie.getAttribute('title').getValueType().isOptional(); // => false\n   * movie.title = undefined; // Error\n   * ```\n   *\n   * @category Methods\n   */\n  isOptional() {\n    return this._isOptional === true;\n  }\n\n  getSanitizers() {\n    return this._sanitizers;\n  }\n\n  /**\n   * Returns the validators associated to the value type.\n   *\n   * @returns A array of [`Validator`](https://layrjs.com/docs/v2/reference/component).\n   *\n   * @example\n   * ```\n   * movie.getAttribute('reference').getValueType().getValidators();\n   * // => [integerValidator, greaterThanValidator]\n   * ```\n   *\n   * @category Methods\n   */\n  getValidators() {\n    return this._validators;\n  }\n\n  /**\n   * @method getItemType\n   *\n   * In case the value type is an array, returns the value type of the items it contains.\n   *\n   * @returns A [ValueType](https://layrjs.com/docs/v2/reference/value-type).\n   *\n   * @example\n   * ```\n   * movie.getAttribute('actors').getValueType().getItemType().toString(); // => 'Actor'\n   * ```\n   *\n   * @category Methods\n   */\n\n  /**\n   * Returns a string representation of the value type.\n   *\n   * @returns A string.\n   *\n   * @example\n   * ```\n   * movie.getAttribute('title').getValueType().toString(); // => 'string'\n   * movie.getAttribute('summary').getValueType().toString(); // => 'string?'\n   * movie.getAttribute('actors').getValueType().toString(); // => 'Actor[]'\n   * ```\n   *\n   * @category Methods\n   */\n  toString(): string {\n    return this.isOptional() ? '?' : '';\n  }\n\n  getScalarType() {\n    return this as ValueType;\n  }\n\n  checkValue(value: unknown, attribute: Attribute) {\n    if (!this._checkValue(value, attribute)) {\n      throw new Error(\n        `Cannot assign a value of an unexpected type (${attribute.describe()}, expected type: '${this.toString()}', received type: '${getTypeOf(\n          value\n        )}')`\n      );\n    }\n  }\n\n  _checkValue(value: unknown, _attribute: Attribute) {\n    return value === undefined ? this.isOptional() : undefined;\n  }\n\n  _traverseAttributes(\n    _iteratee: TraverseAttributesIteratee,\n    _attribute: Attribute,\n    _value: unknown,\n    _options: TraverseAttributesOptions\n  ) {\n    // NOOP\n  }\n\n  _resolveAttributeSelector(\n    normalizedAttributeSelector: AttributeSelector,\n    _attribute: Attribute,\n    _value: unknown,\n    _options: ResolveAttributeSelectorOptions\n  ): AttributeSelector {\n    return normalizedAttributeSelector !== false;\n  }\n\n  sanitizeValue(value: any) {\n    return runSanitizers(this.getSanitizers(), value);\n  }\n\n  isValidValue(value: unknown) {\n    const failedValidators = this.runValidators(value);\n\n    return failedValidators.length === 0;\n  }\n\n  runValidators(value: unknown, _attributeSelector?: AttributeSelector) {\n    const failedValidators = runValidators(this.getValidators(), value);\n\n    const failedValidatorsWithPath = failedValidators.map((failedValidator) => ({\n      validator: failedValidator,\n      path: ''\n    }));\n\n    return failedValidatorsWithPath;\n  }\n\n  serializeValue(value: unknown, _attribute: Attribute, options: SerializeOptions = {}) {\n    return serialize(value, options);\n  }\n\n  canDeserializeInPlace(_attribute: Attribute) {\n    return false;\n  }\n\n  introspect() {\n    const introspectedValueType: IntrospectedValueType = {valueType: this.toString()};\n\n    const validators = this.getValidators();\n\n    if (validators.length > 0) {\n      introspectedValueType.validators = validators;\n    }\n\n    return introspectedValueType;\n  }\n}\n"
  },
  {
    "path": "packages/component/src/sanitization/index.ts",
    "content": "export * from './utilities';\nexport * from './sanitizer-builders';\nexport * from './sanitizer';\n"
  },
  {
    "path": "packages/component/src/sanitization/sanitizer-builders.test.ts",
    "content": "import {isSanitizerInstance} from './sanitizer';\nimport {sanitizers} from './sanitizer-builders';\n\ndescribe('Sanitizer builders', () => {\n  test('Building sanitizers', async () => {\n    let sanitizer = sanitizers.trim();\n\n    expect(isSanitizerInstance(sanitizer));\n    expect(sanitizer.getName()).toBe('trim');\n    expect(typeof sanitizer.getFunction()).toBe('function');\n    expect(sanitizer.getArguments()).toEqual([]);\n\n    sanitizer = sanitizers.compact();\n\n    expect(isSanitizerInstance(sanitizer));\n    expect(sanitizer.getName()).toBe('compact');\n    expect(typeof sanitizer.getFunction()).toBe('function');\n    expect(sanitizer.getArguments()).toEqual([]);\n  });\n\n  test('Running built-in sanitizers', async () => {\n    expect(sanitizers.trim().run('hello')).toBe('hello');\n    expect(sanitizers.trim().run(' hello ')).toBe('hello');\n    expect(sanitizers.trim().run(undefined)).toBe(undefined);\n\n    expect(sanitizers.compact().run(['hello'])).toStrictEqual(['hello']);\n    expect(sanitizers.compact().run(['hello', ''])).toStrictEqual(['hello']);\n    expect(sanitizers.compact().run([''])).toStrictEqual([]);\n    expect(sanitizers.compact().run([])).toStrictEqual([]);\n    expect(sanitizers.compact().run(undefined)).toBe(undefined);\n  });\n});\n"
  },
  {
    "path": "packages/component/src/sanitization/sanitizer-builders.ts",
    "content": "import trim from 'lodash/trim';\nimport compact from 'lodash/compact';\n\nimport {Sanitizer, SanitizerFunction} from './sanitizer';\n\nconst sanitizerFunctions: {[name: string]: SanitizerFunction} = {\n  // Strings\n\n  trim: (value) => (value !== undefined ? trim(value) : undefined),\n\n  // Arrays\n\n  compact: (value) => (value !== undefined ? compact(value) : undefined)\n};\n\nexport type SanitizerBuilder = (...args: any[]) => Sanitizer;\n\nexport const sanitizers: {[name: string]: SanitizerBuilder} = {};\n\nfor (const [name, func] of Object.entries(sanitizerFunctions)) {\n  sanitizers[name] = (...args) => createSanitizer(name, func, args);\n}\n\nfunction createSanitizer(name: string, func: SanitizerFunction, args: any[]) {\n  const numberOfRequiredArguments = func.length - 1;\n  const sanitizerArguments = args.slice(0, numberOfRequiredArguments);\n\n  if (sanitizerArguments.length < numberOfRequiredArguments) {\n    throw new Error(`A required parameter is missing to build the sanitizer '${name}'`);\n  }\n\n  return new Sanitizer(func, {name, arguments: sanitizerArguments});\n}\n"
  },
  {
    "path": "packages/component/src/sanitization/sanitizer.test.ts",
    "content": "import {Sanitizer, isSanitizerInstance, runSanitizers} from './sanitizer';\n\nrunSanitizers;\n\ndescribe('Sanitizer', () => {\n  const trimStart = (value: string) => value.trimStart();\n\n  const trim = (value: string, {start = true, end = true}: {start?: boolean; end?: boolean}) => {\n    if (start) {\n      value = value.trimStart();\n    }\n\n    if (end) {\n      value = value.trimEnd();\n    }\n\n    return value;\n  };\n\n  const upperCaseFirst = (value: string) => value.slice(0, 1).toUpperCase() + value.slice(1);\n\n  test('Creation', async () => {\n    let sanitizer = new Sanitizer(trimStart);\n\n    expect(isSanitizerInstance(sanitizer));\n    expect(sanitizer.getName()).toBe('trimStart');\n    expect(sanitizer.getFunction()).toBe(trimStart);\n    expect(sanitizer.getArguments()).toEqual([]);\n\n    sanitizer = new Sanitizer(trim, {arguments: [{end: false}]});\n\n    expect(isSanitizerInstance(sanitizer));\n    expect(sanitizer.getName()).toBe('trim');\n    expect(sanitizer.getFunction()).toBe(trim);\n    expect(sanitizer.getArguments()).toEqual([{end: false}]);\n  });\n\n  test('Execution', async () => {\n    const trimStartSanitizer = new Sanitizer(trim, {arguments: [{end: false}]});\n    const upperCaseFirstSanitizer = new Sanitizer(upperCaseFirst);\n\n    expect(trimStartSanitizer.run('hello')).toBe('hello');\n    expect(trimStartSanitizer.run(' hello ')).toBe('hello ');\n    expect(upperCaseFirstSanitizer.run('hello')).toBe('Hello');\n\n    expect(runSanitizers([trimStartSanitizer], 'hello')).toBe('hello');\n    expect(runSanitizers([trimStartSanitizer], ' hello ')).toBe('hello ');\n    expect(runSanitizers([trimStartSanitizer, upperCaseFirstSanitizer], ' hello')).toBe('Hello');\n    expect(runSanitizers([upperCaseFirstSanitizer, trimStartSanitizer], ' hello')).toBe('hello');\n  });\n});\n"
  },
  {
    "path": "packages/component/src/sanitization/sanitizer.ts",
    "content": "import {getFunctionName} from 'core-helpers';\n\nexport type SanitizerFunction = (value: any, ...args: any[]) => any;\n\ntype SanitizerOptions = {\n  name?: string;\n  arguments?: any[];\n};\n\n/**\n * A class to handle the sanitization of the component attributes.\n *\n * #### Usage\n *\n * You shouldn't have to create a `Sanitizer` instance directly. Instead, when you define an attribute (using a decorator such as [`@attribute()`](https://layrjs.com/docs/v2/reference/component#attribute-decorator)), you can invoke some [built-in sanitizer builders](https://layrjs.com/docs/v2/reference/sanitizer#built-in-sanitizer-builders) or specify your own [custom sanitization functions](https://layrjs.com/docs/v2/reference/sanitizer#custom-sanitization-functions) that will be automatically transformed into `Sanitizer` instances.\n *\n * **Example:**\n *\n * ```\n * // JS\n *\n * import {Component, attribute, sanitizers} from '﹫layr/component';\n *\n * const {trim, compact} = sanitizers;\n *\n * class Movie extends Component {\n *   // An attribute of type 'string' that is automatically trimmed\n *   ﹫attribute('string', {sanitizers: [trim()]}) title;\n *\n *   // An array attribute for storing non-empty strings\n *   ﹫attribute('string[]', {sanitizers: [compact()], items: {sanitizers: [trim()]}})\n *   tags = [];\n * }\n * ```\n *\n * ```\n * // TS\n *\n * import {Component, attribute, sanitizers} from '﹫layr/component';\n *\n * const {trim, compact} = sanitizers;\n *\n * class Movie extends Component {\n *   // An attribute of type 'string' that is automatically trimmed\n *   ﹫attribute('string', {sanitizers: [trim()]}) title!: string;\n *\n *   // An array attribute for storing non-empty strings\n *   ﹫attribute('string[]', {sanitizers: [compact()], items: {sanitizers: [trim()]}})\n *   tags: string[] = [];\n * }\n * ```\n *\n * In case you want to access the `Sanitizer` instances that were created under the hood, you can do the following:\n *\n * ```\n * const movie = new Movie({ ... });\n *\n * movie.getAttribute('title').getValueType().getSanitizers();\n * // => [trimSanitizer]\n *\n * movie.getAttribute('tags').getValueType().getSanitizers();\n * // => [compactSanitizer]\n *\n * movie.getAttribute('tags').getValueType().getItemType().getSanitizers();\n * // => [trimSanitizer]\n * ```\n *\n * #### Built-In Sanitizer Builders\n *\n * Layr provides some sanitizer builders that can be used when you define your component attributes. See an [example of use](https://layrjs.com/docs/v2/reference/sanitizer#usage) above.\n *\n * ##### Strings\n *\n * The following sanitizer builder can be used to sanitize strings:\n *\n * * `trim()`: Removes leading and trailing whitespace from the string.\n *\n * ##### Arrays\n *\n * The following sanitizer builder can be used to sanitize arrays:\n *\n * * `compact()`: Removes all falsey values from the array. The values `false`, `null`, `0`, `''`, `undefined`, and `NaN` are falsey.\n *\n * #### Custom Sanitization Functions\n *\n * In addition to the [built-in sanitizer builders](https://layrjs.com/docs/v2/reference/sanitizer#built-in-sanitizer-builders), you can sanitize your component attributes with your own custom sanitization functions.\n *\n * A custom sanitization function takes a value as first parameter and returns a new value that is the result of the sanitization.\n *\n * **Example:**\n *\n * ```\n * // JS\n *\n * import {Component, attribute} from '﹫layr/component';\n *\n * class Integer extends Component {\n *   // Ensures that the value is an integer\n *   ﹫attribute('number', {sanitizers: [(value) => Math.round(value)]}) value;\n * }\n * ```\n *\n * ```\n * // TS\n *\n * import {Component, attribute} from '﹫layr/component';\n *\n * class Integer extends Component {\n *   // Ensures that the value is an integer\n *   ﹫attribute('number', {sanitizers: [(value) => Math.round(value)]}) value!: number;\n * }\n * ```\n */\nexport class Sanitizer {\n  _function: SanitizerFunction;\n  _name: string;\n  _arguments: any[];\n\n  constructor(func: SanitizerFunction, options: SanitizerOptions = {}) {\n    let {name, arguments: args = []} = options;\n\n    if (name === undefined) {\n      name = getFunctionName(func) || 'anonymous';\n    }\n\n    this._function = func;\n    this._name = name;\n    this._arguments = args;\n  }\n\n  getFunction() {\n    return this._function;\n  }\n\n  getName() {\n    return this._name;\n  }\n\n  getArguments() {\n    return this._arguments;\n  }\n\n  run(value: any) {\n    return this.getFunction()(value, ...this.getArguments());\n  }\n\n  static isSanitizer(value: any): value is Sanitizer {\n    return isSanitizerInstance(value);\n  }\n}\n\nexport function isSanitizerInstance(value: any): value is Sanitizer {\n  return typeof value?.constructor?.isSanitizer === 'function';\n}\n\nexport function runSanitizers(sanitizers: Sanitizer[], value: any) {\n  for (const sanitizer of sanitizers) {\n    value = sanitizer.run(value);\n  }\n\n  return value;\n}\n"
  },
  {
    "path": "packages/component/src/sanitization/utilities.test.ts",
    "content": "import {Component} from '../component';\nimport {Attribute} from '../properties';\nimport {Sanitizer, isSanitizerInstance} from './sanitizer';\nimport {sanitizers} from './sanitizer-builders';\nimport {normalizeSanitizer} from './utilities';\n\ndescribe('Utilities', () => {\n  test('Normalization', async () => {\n    class TestComponent extends Component {}\n\n    const attribute = new Attribute('testAttribute', TestComponent.prototype);\n\n    let sanitizer: any = new Sanitizer((value) => value > 0);\n    let normalizedSanitizer = normalizeSanitizer(sanitizer, attribute);\n\n    expect(normalizedSanitizer).toBe(sanitizer);\n\n    sanitizer = sanitizers.trim();\n    normalizedSanitizer = normalizeSanitizer(sanitizer, attribute);\n\n    expect(normalizedSanitizer).toBe(sanitizer);\n\n    sanitizer = sanitizers.trim;\n\n    expect(() => normalizeSanitizer(sanitizer, attribute)).toThrow(\n      \"The specified sanitizer is a sanitizer builder that has not been called (attribute: 'TestComponent.prototype.testAttribute')\"\n    );\n\n    sanitizer = (value: number) => value > 0;\n    normalizedSanitizer = normalizeSanitizer(sanitizer, attribute);\n\n    expect(isSanitizerInstance(normalizedSanitizer));\n    expect(normalizedSanitizer.getName()).toBe('sanitizer');\n    expect(normalizedSanitizer.getFunction()).toBe(sanitizer);\n    expect(normalizedSanitizer.getArguments()).toEqual([]);\n  });\n});\n"
  },
  {
    "path": "packages/component/src/sanitization/utilities.ts",
    "content": "import {sanitizers, SanitizerBuilder} from './sanitizer-builders';\nimport {Sanitizer, SanitizerFunction, isSanitizerInstance} from './sanitizer';\nimport type {Attribute} from '../properties';\n\nexport function normalizeSanitizer(sanitizer: Sanitizer | SanitizerFunction, attribute: Attribute) {\n  if (isSanitizerInstance(sanitizer)) {\n    return sanitizer;\n  }\n\n  if (typeof sanitizer !== 'function') {\n    throw new Error(`The specified sanitizer is not a function (${attribute.describe()})`);\n  }\n\n  if (Object.values(sanitizers).includes((sanitizer as unknown) as SanitizerBuilder)) {\n    throw new Error(\n      `The specified sanitizer is a sanitizer builder that has not been called (${attribute.describe()})`\n    );\n  }\n\n  return new Sanitizer(sanitizer);\n}\n"
  },
  {
    "path": "packages/component/src/serialization.test.ts",
    "content": "import {Component, ComponentSet} from './component';\nimport {EmbeddedComponent} from './embedded-component';\nimport {attribute, primaryIdentifier, secondaryIdentifier, provide} from './decorators';\nimport {serialize} from './serialization';\n\ndescribe('Serialization', () => {\n  test('Component classes', async () => {\n    class BaseMovie extends Component {}\n\n    expect(BaseMovie.serialize()).toStrictEqual({__component: 'typeof BaseMovie'});\n\n    class Movie extends BaseMovie {\n      @attribute() static limit = 100;\n      @attribute() static offset: number;\n    }\n\n    expect(Movie.serialize()).toStrictEqual({\n      __component: 'typeof Movie',\n      limit: 100,\n      offset: {__undefined: true}\n    });\n\n    expect(Movie.serialize({attributeSelector: {limit: true}})).toStrictEqual({\n      __component: 'typeof Movie',\n      limit: 100\n    });\n\n    expect(Movie.serialize({returnComponentReferences: true})).toStrictEqual({\n      __component: 'typeof Movie'\n    });\n\n    // - Value sourcing -\n\n    Movie.getAttribute('limit').setValueSource('client');\n\n    expect(Movie.serialize()).toStrictEqual({\n      __component: 'typeof Movie',\n      limit: 100,\n      offset: {__undefined: true}\n    });\n    expect(Movie.serialize({target: 'client'})).toStrictEqual({\n      __component: 'typeof Movie',\n      offset: {__undefined: true}\n    });\n\n    Movie.getAttribute('offset').setValueSource('client');\n\n    expect(Movie.serialize({target: 'client'})).toStrictEqual({\n      __component: 'typeof Movie'\n    });\n\n    // --- With referenced components ---\n\n    class Cinema extends Component {\n      @attribute() static limit = 100;\n\n      @attribute() static MovieClass = Movie;\n    }\n\n    expect(Cinema.serialize()).toStrictEqual({\n      __component: 'typeof Cinema',\n      limit: 100,\n      MovieClass: {__component: 'typeof Movie'}\n    });\n\n    let componentDependencies: ComponentSet = new Set();\n\n    expect(Cinema.serialize({componentDependencies})).toStrictEqual({\n      __component: 'typeof Cinema',\n      limit: 100,\n      MovieClass: {__component: 'typeof Movie'}\n    });\n    expect(Array.from(componentDependencies)).toStrictEqual([]);\n\n    componentDependencies = new Set();\n\n    expect(\n      Cinema.serialize({returnComponentReferences: true, componentDependencies})\n    ).toStrictEqual({\n      __component: 'typeof Cinema'\n    });\n    expect(Array.from(componentDependencies)).toStrictEqual([]);\n  });\n\n  test('Component instances', async () => {\n    class Person extends EmbeddedComponent {\n      @attribute() name?: string;\n      @attribute() country?: string;\n    }\n\n    class Director extends Person {}\n    class Actor extends Person {}\n\n    class Movie extends Component {\n      @provide() static Director = Director;\n      @provide() static Actor = Actor;\n\n      @attribute() title = '';\n      @attribute('Director?') director?: Director;\n      @attribute('Actor[]') actors = new Array<Actor>();\n    }\n\n    let movie = new Movie();\n\n    expect(movie.serialize()).toStrictEqual({\n      __component: 'Movie',\n      __new: true,\n      title: '',\n      director: {__undefined: true},\n      actors: []\n    });\n\n    expect(movie.serialize({attributeSelector: {title: true}})).toStrictEqual({\n      __component: 'Movie',\n      __new: true,\n      title: ''\n    });\n\n    expect(movie.serialize({includeIsNewMarks: false})).toStrictEqual({\n      __component: 'Movie',\n      title: '',\n      director: {__undefined: true},\n      actors: []\n    });\n\n    movie = Movie.instantiate();\n\n    expect(movie.serialize()).toStrictEqual({\n      __component: 'Movie'\n    });\n\n    expect(movie.serialize({includeComponentTypes: false})).toStrictEqual({});\n\n    movie.title = 'Inception';\n\n    expect(movie.serialize()).toStrictEqual({\n      __component: 'Movie',\n      title: 'Inception'\n    });\n\n    expect(movie.serialize({includeComponentTypes: false})).toStrictEqual({\n      title: 'Inception'\n    });\n\n    // - Value sourcing -\n\n    movie = Movie.instantiate();\n    movie.getAttribute('title').setValue('Inception', {source: 'client'});\n\n    expect(movie.serialize()).toStrictEqual({\n      __component: 'Movie',\n      title: 'Inception'\n    });\n    expect(movie.serialize({target: 'client'})).toStrictEqual({\n      __component: 'Movie'\n    });\n\n    // --- With an embedded component ---\n\n    movie.director = new Director({name: 'Christopher Nolan'});\n\n    expect(movie.serialize()).toStrictEqual({\n      __component: 'Movie',\n      title: 'Inception',\n      director: {\n        __component: 'Director',\n        __new: true,\n        name: 'Christopher Nolan',\n        country: {__undefined: true}\n      }\n    });\n\n    expect(\n      movie.serialize({attributeSelector: {title: true, director: {name: true}}})\n    ).toStrictEqual({\n      __component: 'Movie',\n      title: 'Inception',\n      director: {__component: 'Director', __new: true, name: 'Christopher Nolan'}\n    });\n\n    expect(movie.serialize({attributeSelector: {title: true, director: {}}})).toStrictEqual({\n      __component: 'Movie',\n      title: 'Inception',\n      director: {__component: 'Director', __new: true}\n    });\n\n    expect(movie.serialize({includeIsNewMarks: false})).toStrictEqual({\n      __component: 'Movie',\n      title: 'Inception',\n      director: {__component: 'Director', name: 'Christopher Nolan', country: {__undefined: true}}\n    });\n\n    expect(\n      movie.serialize({\n        attributeFilter(attribute) {\n          expect(this).toBe(movie);\n          expect(attribute.getParent()).toBe(movie);\n          return attribute.getName() === 'title';\n        }\n      })\n    ).toStrictEqual({\n      __component: 'Movie',\n      title: 'Inception'\n    });\n\n    expect(\n      await movie.serialize({\n        async attributeFilter(attribute) {\n          expect(this).toBe(movie);\n          expect(attribute.getParent()).toBe(movie);\n          return attribute.getName() === 'title';\n        }\n      })\n    ).toStrictEqual({\n      __component: 'Movie',\n      title: 'Inception'\n    });\n\n    // - Value sourcing -\n\n    const director = Director.instantiate();\n    director.getAttribute('name').setValue('Christopher Nolan', {source: 'client'});\n    director.getAttribute('country').setValue('USA', {source: 'client'});\n\n    movie.getAttribute('director').setValue(director, {source: 'client'});\n\n    expect(movie.serialize()).toStrictEqual({\n      __component: 'Movie',\n      title: 'Inception',\n      director: {__component: 'Director', name: 'Christopher Nolan', country: 'USA'}\n    });\n\n    expect(movie.serialize({target: 'client'})).toStrictEqual({__component: 'Movie'});\n\n    movie.director.country = 'US';\n\n    expect(movie.serialize({target: 'client'})).toStrictEqual({\n      __component: 'Movie',\n      director: {__component: 'Director', country: 'US'}\n    });\n\n    // --- With an array of embedded components ---\n\n    movie.actors = [new Actor({name: 'Leonardo DiCaprio'})];\n\n    expect(movie.serialize({attributeSelector: {actors: true}})).toStrictEqual({\n      __component: 'Movie',\n      actors: [\n        {__component: 'Actor', __new: true, name: 'Leonardo DiCaprio', country: {__undefined: true}}\n      ]\n    });\n\n    // - Value sourcing -\n\n    const actor = Actor.instantiate();\n    actor.getAttribute('name').setValue('Leonardo DiCaprio', {source: 'client'});\n    actor.getAttribute('country').setValue('USA', {source: 'client'});\n\n    movie.getAttribute('actors').setValue([actor], {source: 'client'});\n\n    expect(movie.serialize({attributeSelector: {actors: true}})).toStrictEqual({\n      __component: 'Movie',\n      actors: [{__component: 'Actor', name: 'Leonardo DiCaprio', country: 'USA'}]\n    });\n\n    expect(movie.serialize({attributeSelector: {actors: true}, target: 'client'})).toStrictEqual({\n      __component: 'Movie'\n    });\n\n    movie.actors[0].country = 'US';\n\n    expect(movie.serialize({attributeSelector: {actors: true}, target: 'client'})).toStrictEqual({\n      __component: 'Movie',\n      actors: [{__component: 'Actor', name: 'Leonardo DiCaprio', country: 'US'}]\n    });\n  });\n\n  test('Identifiable component instances', async () => {\n    class Movie extends Component {\n      @primaryIdentifier() id!: string;\n      @secondaryIdentifier() slug!: string;\n      @attribute('string') title = '';\n    }\n\n    let movie = Movie.fork().instantiate({id: 'abc123'});\n    movie.title = 'Inception';\n\n    expect(movie.serialize()).toEqual({\n      __component: 'Movie',\n      id: 'abc123',\n      title: 'Inception'\n    });\n\n    expect(movie.serialize({returnComponentReferences: true})).toEqual({\n      __component: 'Movie',\n      id: 'abc123'\n    });\n\n    movie = Movie.fork().instantiate({slug: 'inception'});\n    movie.title = 'Inception';\n\n    expect(movie.serialize()).toEqual({\n      __component: 'Movie',\n      slug: 'inception',\n      title: 'Inception'\n    });\n\n    expect(movie.serialize({returnComponentReferences: true})).toEqual({\n      __component: 'Movie',\n      slug: 'inception'\n    });\n\n    movie = Movie.fork().instantiate({id: 'abc123'});\n    movie.slug = 'inception';\n    movie.title = 'Inception';\n\n    expect(movie.serialize()).toEqual({\n      __component: 'Movie',\n      id: 'abc123',\n      slug: 'inception',\n      title: 'Inception'\n    });\n\n    expect(movie.serialize({returnComponentReferences: true})).toEqual({\n      __component: 'Movie',\n      id: 'abc123'\n    });\n\n    // - Value sourcing -\n\n    movie = Movie.fork().instantiate({id: 'abc123'}, {source: 'client'});\n    movie.getAttribute('title').setValue('Inception', {source: 'client'});\n\n    expect(movie.serialize()).toStrictEqual({\n      __component: 'Movie',\n      id: 'abc123',\n      title: 'Inception'\n    });\n    expect(movie.serialize({target: 'client'})).toStrictEqual({\n      __component: 'Movie',\n      id: 'abc123'\n    });\n\n    // --- With referenced identifiable component instances ---\n\n    class Cinema extends Component {\n      @provide() static Movie = Movie;\n\n      @primaryIdentifier() id!: string;\n      @attribute('string') name = '';\n      @attribute('Movie[]') movies!: Movie[];\n    }\n\n    movie = Movie.instantiate({id: 'abc123'});\n    movie.title = 'Inception';\n\n    const cinema = Cinema.instantiate({id: 'xyz456'});\n    cinema.name = 'Paradiso';\n    cinema.movies = [movie];\n\n    expect(cinema.serialize()).toEqual({\n      __component: 'Cinema',\n      id: 'xyz456',\n      name: 'Paradiso',\n      movies: [{__component: 'Movie', id: 'abc123'}]\n    });\n\n    let componentDependencies: ComponentSet = new Set();\n\n    expect(cinema.serialize({componentDependencies})).toEqual({\n      __component: 'Cinema',\n      id: 'xyz456',\n      name: 'Paradiso',\n      movies: [{__component: 'Movie', id: 'abc123'}]\n    });\n    expect(Array.from(componentDependencies)).toEqual([Cinema, Movie]);\n\n    componentDependencies = new Set();\n\n    expect(cinema.serialize({returnComponentReferences: true, componentDependencies})).toEqual({\n      __component: 'Cinema',\n      id: 'xyz456'\n    });\n    expect(Array.from(componentDependencies)).toEqual([Cinema, Movie]);\n\n    // - With an array of components -\n\n    const serializedComponents: ComponentSet = new Set();\n    componentDependencies = new Set();\n\n    expect(\n      serialize([cinema, movie, movie], {serializedComponents, componentDependencies})\n    ).toEqual([\n      {\n        __component: 'Cinema',\n        id: 'xyz456',\n        name: 'Paradiso',\n        movies: [{__component: 'Movie', id: 'abc123'}]\n      },\n      {__component: 'Movie', id: 'abc123', title: 'Inception'},\n      {__component: 'Movie', id: 'abc123'}\n    ]);\n    expect(Array.from(serializedComponents)).toEqual([cinema, movie]);\n    expect(Array.from(componentDependencies)).toEqual([Cinema, Movie]);\n\n    // - Using 'returnComponentReferences' option -\n\n    expect(\n      serialize(\n        {\n          '<=': cinema,\n          'play=>': {'()': [movie]}\n        },\n        {returnComponentReferences: true}\n      )\n    ).toEqual({\n      '<=': {__component: 'Cinema', id: 'xyz456'},\n      'play=>': {'()': [{__component: 'Movie', id: 'abc123'}]}\n    });\n  });\n\n  test('Functions', async () => {\n    function sum(a: number, b: number) {\n      return a + b;\n    }\n\n    expect(serialize(sum)).toStrictEqual({});\n    expect(trimSerializedFunction(serialize(sum, {serializeFunctions: true}))).toStrictEqual({\n      __function: 'function sum(a, b) {\\nreturn a + b;\\n}'\n    });\n\n    sum.displayName = 'sum';\n\n    expect(serialize(sum)).toStrictEqual({displayName: 'sum'});\n    expect(trimSerializedFunction(serialize(sum, {serializeFunctions: true}))).toStrictEqual({\n      __function: 'function sum(a, b) {\\nreturn a + b;\\n}',\n      displayName: 'sum'\n    });\n\n    function trimSerializedFunction(serializedFunction: any) {\n      return {\n        ...serializedFunction,\n        __function: serializedFunction.__function.replace(/\\n +/g, '\\n')\n      };\n    }\n  });\n});\n"
  },
  {
    "path": "packages/component/src/serialization.ts",
    "content": "import {\n  serialize as simpleSerialize,\n  SerializeOptions as SimpleSerializeOptions,\n  SerializeResult\n} from 'simple-serialization';\nimport {possiblyAsync} from 'possibly-async';\nimport {isES2015Class} from 'core-helpers';\n\nimport type {ComponentSet} from './component';\nimport type {PropertyFilter, AttributeSelector, ValueSource} from './properties';\nimport {isValidatorInstance} from './validation/validator';\nimport {isComponentClassOrInstance} from './utilities';\n\nexport type SerializeOptions = SimpleSerializeOptions & {\n  attributeSelector?: AttributeSelector;\n  attributeFilter?: PropertyFilter;\n  serializedComponents?: ComponentSet;\n  componentDependencies?: ComponentSet;\n  serializeFunctions?: boolean;\n  returnComponentReferences?: boolean;\n  ignoreEmptyComponents?: boolean;\n  includeComponentTypes?: boolean;\n  includeIsNewMarks?: boolean;\n  includeReferencedComponents?: boolean;\n  target?: ValueSource;\n};\n\n/**\n * Serializes any type of values including objects, arrays, dates, and components (using Component's `serialize()` [class method](https://layrjs.com/docs/v2/reference/component#serialize-class-method) and [instance method](https://layrjs.com/docs/v2/reference/component#serialize-instance-method)).\n *\n * @param value A value of any type.\n * @param [options.attributeFilter] A (possibly async) function used to filter the component attributes to be serialized. The function is invoked for each attribute with an [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) instance as first argument.\n * @param [options.target] The target of the serialization (default: `undefined`).\n *\n * @returns The serialized value.\n *\n * @example\n * ```\n * import {serialize} from '﹫layr/component';\n *\n * const data = {\n *   createdOn: new Date(),\n *   updatedOn: undefined,\n *   movie: new Movie({title: 'Inception'})\n * };\n *\n * console.log(serialize(data));\n *\n * // Should output something like:\n * // {\n * //   createdOn: {__date: \"2020-07-18T23:43:33.778Z\"},\n * //   updatedOn: {__undefined: true},\n * //   movie: {__component: 'Movie', title: 'Inception'}\n * // }\n * ```\n *\n * @category Serialization\n * @possiblyasync\n */\nexport function serialize<Value>(value: Value, options?: SerializeOptions): SerializeResult<Value>;\nexport function serialize(value: any, options: SerializeOptions = {}) {\n  const {\n    serializedComponents = new Set(),\n    objectSerializer: originalObjectSerializer,\n    functionSerializer: originalFunctionSerializer,\n    serializeFunctions = false,\n    ...otherOptions\n  } = options;\n\n  const objectSerializer = function (object: object): object | void {\n    if (originalObjectSerializer !== undefined) {\n      const serializedObject = originalObjectSerializer(object);\n\n      if (serializedObject !== undefined) {\n        return serializedObject;\n      }\n    }\n\n    if (isComponentClassOrInstance(object)) {\n      return object.serialize({...options, serializedComponents});\n    }\n\n    if (isValidatorInstance(object)) {\n      return object.serialize(serialize);\n    }\n  };\n\n  let functionSerializer: SerializeOptions['functionSerializer'];\n\n  if (serializeFunctions) {\n    functionSerializer = function (func) {\n      if (originalFunctionSerializer !== undefined) {\n        const serializedFunction = originalFunctionSerializer(func);\n\n        if (serializedFunction !== undefined) {\n          return serializedFunction;\n        }\n      }\n\n      if (isES2015Class(func)) {\n        throw new Error('Cannot serialize a class');\n      }\n\n      const functionCode = serializeFunction(func);\n\n      const serializedFunction = {__function: functionCode};\n\n      return possiblyAsync(\n        possiblyAsync.mapValues(func as any, (attributeValue) =>\n          simpleSerialize(attributeValue, {...otherOptions, objectSerializer, functionSerializer})\n        ),\n        (serializedAttributes) => {\n          Object.assign(serializedFunction, serializedAttributes);\n          return serializedFunction;\n        }\n      );\n    };\n  }\n\n  return simpleSerialize(value, {...otherOptions, objectSerializer, functionSerializer});\n}\n\nexport function serializeFunction(func: Function) {\n  let sourceCode = func.toString();\n\n  // Clean functions generated by `new Function()`\n  if (sourceCode.startsWith('function anonymous(\\n)')) {\n    sourceCode = 'function ()' + sourceCode.slice('function anonymous(\\n)'.length);\n  }\n\n  return sourceCode;\n}\n"
  },
  {
    "path": "packages/component/src/utilities.test.ts",
    "content": "import {Component} from './component';\nimport {\n  isComponentClass,\n  isComponentInstance,\n  isComponentClassOrInstance,\n  isComponentName,\n  isComponentType\n} from './utilities';\n\ndescribe('Utilities', () => {\n  test('isComponentClass()', async () => {\n    expect(isComponentClass(undefined)).toBe(false);\n    expect(isComponentClass(null)).toBe(false);\n    expect(isComponentClass(true)).toBe(false);\n    expect(isComponentClass(1)).toBe(false);\n    expect(isComponentClass({})).toBe(false);\n\n    class Movie extends Component {}\n\n    expect(isComponentClass(Movie)).toBe(true);\n    expect(isComponentClass(Movie.prototype)).toBe(false);\n\n    const movie = new Movie();\n\n    expect(isComponentClass(movie)).toBe(false);\n  });\n\n  test('isComponentInstance()', async () => {\n    expect(isComponentInstance(undefined)).toBe(false);\n    expect(isComponentInstance(null)).toBe(false);\n    expect(isComponentInstance(true)).toBe(false);\n    expect(isComponentInstance(1)).toBe(false);\n    expect(isComponentInstance({})).toBe(false);\n\n    class Movie extends Component {}\n\n    expect(isComponentInstance(Movie.prototype)).toBe(true);\n\n    const movie = new Movie();\n\n    expect(isComponentInstance(movie)).toBe(true);\n  });\n\n  test('isComponentClassOrInstance()', async () => {\n    expect(isComponentClassOrInstance(undefined)).toBe(false);\n    expect(isComponentClassOrInstance(null)).toBe(false);\n    expect(isComponentClassOrInstance(true)).toBe(false);\n    expect(isComponentClassOrInstance(1)).toBe(false);\n    expect(isComponentClassOrInstance({})).toBe(false);\n\n    class Movie extends Component {}\n\n    expect(isComponentClassOrInstance(Movie)).toBe(true);\n    expect(isComponentClassOrInstance(Movie.prototype)).toBe(true);\n\n    const movie = new Movie();\n\n    expect(isComponentClassOrInstance(movie)).toBe(true);\n  });\n\n  test('isComponentName()', async () => {\n    expect(isComponentName('Movie')).toBe(true);\n    expect(isComponentName('Movie2')).toBe(true);\n    expect(isComponentName('MotionPicture')).toBe(true);\n    expect(isComponentName('Prefix_Movie')).toBe(true);\n\n    expect(isComponentName('$Movie')).toBe(false);\n    expect(isComponentName('_Movie')).toBe(false);\n    expect(isComponentName('Movie!')).toBe(false);\n    expect(isComponentName('1Place')).toBe(false);\n  });\n\n  test('isComponentType()', async () => {\n    expect(isComponentType('typeof Movie')).toBe('componentClassType');\n    expect(isComponentType('Movie')).toBe('componentInstanceType');\n    expect(isComponentType('typeof MotionPicture')).toBe('componentClassType');\n    expect(isComponentType('MotionPicture')).toBe('componentInstanceType');\n    expect(isComponentType('typeof Prefix_Movie')).toBe('componentClassType');\n    expect(isComponentType('Prefix_Movie')).toBe('componentInstanceType');\n\n    expect(isComponentType('$Movie')).toBe(false);\n    expect(isComponentType('_Movie')).toBe(false);\n    expect(isComponentType('Movie!')).toBe(false);\n    expect(isComponentType('1Place')).toBe(false);\n\n    expect(isComponentType('typeof Movie', {allowClasses: false})).toBe(false);\n    expect(isComponentType('Movie', {allowInstances: false})).toBe(false);\n  });\n});\n"
  },
  {
    "path": "packages/component/src/utilities.ts",
    "content": "import {isES2015Class, getFunctionName, getTypeOf} from 'core-helpers';\nimport compact from 'lodash/compact';\n\nimport type {Component, ComponentMixin} from './component';\n\n/**\n * Returns whether the specified value is a component class.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isComponentClass(value: any): value is typeof Component {\n  return typeof value?.isComponent === 'function';\n}\n\n/**\n * Returns whether the specified value is a component instance.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isComponentInstance(value: any): value is Component {\n  return typeof value?.constructor?.isComponent === 'function';\n}\n\n/**\n * Returns whether the specified value is a component class or instance.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isComponentClassOrInstance(value: any): value is typeof Component | Component {\n  return (\n    typeof value?.isComponent === 'function' ||\n    typeof value?.constructor?.isComponent === 'function'\n  );\n}\n\n/**\n * Throws an error if the specified value is not a component class.\n *\n * @param value A value of any type.\n *\n * @category Utilities\n */\nexport function assertIsComponentClass(value: any): asserts value is typeof Component {\n  if (!isComponentClass(value)) {\n    throw new Error(\n      `Expected a component class, but received a value of type '${getTypeOf(value)}'`\n    );\n  }\n}\n\n/**\n * Throws an error if the specified value is not a component instance.\n *\n * @param value A value of any type.\n *\n * @category Utilities\n */\nexport function assertIsComponentInstance(value: any): asserts value is Component {\n  if (!isComponentInstance(value)) {\n    throw new Error(\n      `Expected a component instance, but received a value of type '${getTypeOf(value)}'`\n    );\n  }\n}\n\n/**\n * Throws an error if the specified value is not a component class or instance.\n *\n * @param value A value of any type.\n *\n * @category Utilities\n */\nexport function assertIsComponentClassOrInstance(\n  value: any\n): asserts value is typeof Component | Component {\n  if (!isComponentClassOrInstance(value)) {\n    throw new Error(\n      `Expected a component class or instance, but received a value of type '${getTypeOf(value)}'`\n    );\n  }\n}\n\n/**\n * Ensures that the specified component is a class. If you specify a component instance (or prototype), the class of the component is returned. If you specify a component class, it is returned as is.\n *\n * @param component A component class or instance.\n *\n * @returns A component class.\n *\n * @example\n * ```\n * ensureComponentClass(movie) => Movie\n * ensureComponentClass(Movie.prototype) => Movie\n * ensureComponentClass(Movie) => Movie\n * ```\n *\n * @category Utilities\n */\nexport function ensureComponentClass(component: any) {\n  if (isComponentClass(component)) {\n    return component;\n  }\n\n  if (isComponentInstance(component)) {\n    return component.constructor as typeof Component;\n  }\n\n  throw new Error(\n    `Expected a component class or instance, but received a value of type '${getTypeOf(component)}'`\n  );\n}\n\n/**\n * Ensures that the specified component is an instance (or prototype). If you specify a component class, the component prototype is returned. If you specify a component instance (or prototype), it is returned as is.\n *\n * @param component A component class or instance.\n *\n * @returns A component instance (or prototype).\n *\n * @example\n * ```\n * ensureComponentInstance(Movie) => Movie.prototype\n * ensureComponentInstance(Movie.prototype) => Movie.prototype\n * ensureComponentInstance(movie) => movie\n * ```\n *\n * @category Utilities\n */\nexport function ensureComponentInstance(component: any) {\n  if (isComponentClass(component)) {\n    return component.prototype;\n  }\n\n  if (isComponentInstance(component)) {\n    return component;\n  }\n\n  throw new Error(\n    `Expected a component class or instance, but received a value of type '${getTypeOf(component)}'`\n  );\n}\n\nconst COMPONENT_NAME_PATTERN = /^[A-Z][A-Za-z0-9_]*$/;\n\n/**\n * Returns whether the specified string is a valid component name. The rule is the same as for typical JavaScript class names.\n *\n * @param name The string to check.\n *\n * @returns A boolean.\n *\n * @example\n * ```\n * isComponentName('Movie') => true\n * isComponentName('Movie123') => true\n * isComponentName('Awesome_Movie') => true\n * isComponentName('123Movie') => false\n * isComponentName('Awesome-Movie') => false\n * isComponentName('movie') => false\n * ```\n *\n * @category Utilities\n */\nexport function isComponentName(name: string) {\n  return COMPONENT_NAME_PATTERN.test(name);\n}\n\n/**\n * Throws an error if the specified string is not a valid component name.\n *\n * @param name The string to check.\n *\n * @category Utilities\n */\nexport function assertIsComponentName(name: string) {\n  if (name === '') {\n    throw new Error('A component name cannot be empty');\n  }\n\n  if (!isComponentName(name)) {\n    throw new Error(`The specified component name ('${name}') is invalid`);\n  }\n}\n\n/**\n * Transforms a component class type into a component name.\n *\n * @param name A string representing a component class type.\n *\n * @returns A component name.\n *\n * @example\n * ```\n * getComponentNameFromComponentClassType('typeof Movie') => 'Movie'\n * ```\n *\n * @category Utilities\n */\nexport function getComponentNameFromComponentClassType(type: string) {\n  assertIsComponentType(type, {allowInstances: false});\n\n  return type.slice('typeof '.length);\n}\n\n/**\n * Transforms a component instance type into a component name.\n *\n * @param name A string representing a component instance type.\n *\n * @returns A component name.\n *\n * @example\n * ```\n * getComponentNameFromComponentInstanceType('Movie') => 'Movie'\n * ```\n *\n * @category Utilities\n */\nexport function getComponentNameFromComponentInstanceType(type: string) {\n  assertIsComponentType(type, {allowClasses: false});\n\n  return type;\n}\n\nconst COMPONENT_CLASS_TYPE_PATTERN = /^typeof [A-Z][A-Za-z0-9_]*$/;\nconst COMPONENT_INSTANCE_TYPE_PATTERN = /^[A-Z][A-Za-z0-9_]*$/;\n\n/**\n * Returns whether the specified string is a valid component type.\n *\n * @param name The string to check.\n * @param [options.allowClasses] A boolean specifying whether component class types are allowed (default: `true`).\n * @param [options.allowInstances] A boolean specifying whether component instance types are allowed (default: `true`).\n *\n * @returns A boolean.\n *\n * @example\n * ```\n * isComponentType('typeof Movie') => true\n * isComponentType('Movie') => true\n * isComponentType('typeof Awesome-Movie') => false\n * isComponentType('movie') => false\n * isComponentType('typeof Movie', {allowClasses: false}) => false\n * isComponentType('Movie', {allowInstances: false}) => false\n * ```\n *\n * @category Utilities\n */\nexport function isComponentType(type: string, {allowClasses = true, allowInstances = true} = {}) {\n  if (allowClasses && COMPONENT_CLASS_TYPE_PATTERN.test(type)) {\n    return 'componentClassType';\n  }\n\n  if (allowInstances && COMPONENT_INSTANCE_TYPE_PATTERN.test(type)) {\n    return 'componentInstanceType';\n  }\n\n  return false;\n}\n\n/**\n * Throws an error if the specified string is not a valid component type.\n *\n * @param name The string to check.\n * @param [options.allowClasses] A boolean specifying whether component class types are allowed (default: `true`).\n * @param [options.allowInstances] A boolean specifying whether component instance types are allowed (default: `true`).\n *\n * @category Utilities\n */\nexport function assertIsComponentType(\n  type: string,\n  {allowClasses = true, allowInstances = true} = {}\n) {\n  if (type === '') {\n    throw new Error('A component type cannot be empty');\n  }\n\n  const isComponentTypeResult = isComponentType(type, {allowClasses, allowInstances});\n\n  if (isComponentTypeResult === false) {\n    throw new Error(`The specified component type ('${type}') is invalid`);\n  }\n\n  return isComponentTypeResult;\n}\n\n/**\n * Transforms a component name into a component class type.\n *\n * @param name A component name.\n *\n * @returns A component class type.\n *\n * @example\n * ```\n * getComponentClassTypeFromComponentName('Movie') => 'typeof Movie'\n * ```\n *\n * @category Utilities\n */\nexport function getComponentClassTypeFromComponentName(name: string) {\n  assertIsComponentName(name);\n\n  return `typeof ${name}`;\n}\n\n/**\n * Transforms a component name into a component instance type.\n *\n * @param name A component name.\n *\n * @returns A component instance type.\n *\n * @example\n * ```\n * getComponentInstanceTypeFromComponentName('Movie') => 'Movie'\n * ```\n *\n * @category Utilities\n */\nexport function getComponentInstanceTypeFromComponentName(name: string) {\n  assertIsComponentName(name);\n\n  return name;\n}\n\ntype ComponentMap = {[name: string]: typeof Component};\n\nexport function createComponentMap(components: typeof Component[] = []) {\n  const componentMap: ComponentMap = Object.create(null);\n\n  for (const component of components) {\n    assertIsComponentClass(component);\n\n    componentMap[component.getComponentName()] = component;\n  }\n\n  return componentMap;\n}\n\nexport function getComponentFromComponentMap(componentMap: ComponentMap, name: string) {\n  assertIsComponentName(name);\n\n  const component = componentMap[name];\n\n  if (component === undefined) {\n    throw new Error(`The component '${name}' is unknown`);\n  }\n\n  return component;\n}\n\nexport function isComponentMixin(value: any): value is ComponentMixin {\n  return typeof value === 'function' && getFunctionName(value) !== '' && !isES2015Class(value);\n}\n\nexport function assertIsComponentMixin(value: any): asserts value is ComponentMixin {\n  if (!isComponentMixin(value)) {\n    throw new Error(\n      `Expected a component mixin, but received a value of type '${getTypeOf(value)}'`\n    );\n  }\n}\n\nexport function composeDescription(description: string[]) {\n  let composedDescription = compact(description).join(', ');\n\n  if (composedDescription !== '') {\n    composedDescription = ` (${composedDescription})`;\n  }\n\n  return composedDescription;\n}\n\nexport function joinAttributePath(path: [string?, string?]) {\n  const compactedPath = compact(path);\n\n  if (compactedPath.length === 0) {\n    return '';\n  }\n\n  if (compactedPath.length === 1) {\n    return compactedPath[0];\n  }\n\n  const [first, second] = compactedPath;\n\n  if (second.startsWith('[')) {\n    return `${first}${second}`;\n  }\n\n  return `${first}.${second}`;\n}\n"
  },
  {
    "path": "packages/component/src/validation/index.ts",
    "content": "export * from './utilities';\nexport * from './validator-builders';\nexport * from './validator';\n"
  },
  {
    "path": "packages/component/src/validation/utilities.test.ts",
    "content": "import {Component} from '../component';\nimport {Attribute} from '../properties';\nimport {Validator, isValidatorInstance} from './validator';\nimport {validators} from './validator-builders';\nimport {normalizeValidator} from './utilities';\n\ndescribe('Utilities', () => {\n  test('Normalization', async () => {\n    class TestComponent extends Component {}\n\n    const attribute = new Attribute('testAttribute', TestComponent.prototype);\n\n    let validator: any = new Validator((value) => value > 0);\n    let normalizedValidator = normalizeValidator(validator, attribute);\n\n    expect(normalizedValidator).toBe(validator);\n\n    validator = validators.notEmpty();\n    normalizedValidator = normalizeValidator(validator, attribute);\n\n    expect(normalizedValidator).toBe(validator);\n\n    validator = validators.notEmpty;\n\n    expect(() => normalizeValidator(validator, attribute)).toThrow(\n      \"The specified validator is a validator builder that has not been called (attribute: 'TestComponent.prototype.testAttribute')\"\n    );\n\n    validator = (value: number) => value > 0;\n    normalizedValidator = normalizeValidator(validator, attribute);\n\n    expect(isValidatorInstance(normalizedValidator));\n    expect(normalizedValidator.getName()).toBe('validator');\n    expect(normalizedValidator.getFunction()).toBe(validator);\n    expect(normalizedValidator.getArguments()).toEqual([]);\n    expect(normalizedValidator.getMessage()).toBe('The validator `validator()` failed');\n  });\n});\n"
  },
  {
    "path": "packages/component/src/validation/utilities.ts",
    "content": "import {validators, ValidatorBuilder} from './validator-builders';\nimport {Validator, ValidatorFunction, isValidatorInstance} from './validator';\nimport type {Attribute} from '../properties';\n\nexport function normalizeValidator(validator: Validator | ValidatorFunction, attribute: Attribute) {\n  if (isValidatorInstance(validator)) {\n    return validator;\n  }\n\n  if (typeof validator !== 'function') {\n    throw new Error(`The specified validator is not a function (${attribute.describe()})`);\n  }\n\n  if (Object.values(validators).includes((validator as unknown) as ValidatorBuilder)) {\n    throw new Error(\n      `The specified validator is a validator builder that has not been called (${attribute.describe()})`\n    );\n  }\n\n  return new Validator(validator);\n}\n"
  },
  {
    "path": "packages/component/src/validation/validator-builders.test.ts",
    "content": "import {isValidatorInstance} from './validator';\nimport {validators} from './validator-builders';\n\ndescribe('Validator builders', () => {\n  test('Building validators', async () => {\n    let validator = validators.notEmpty();\n\n    expect(isValidatorInstance(validator));\n    expect(validator.getName()).toBe('notEmpty');\n    expect(typeof validator.getFunction()).toBe('function');\n    expect(validator.getArguments()).toEqual([]);\n    expect(validator.getMessage()).toBe('The validator `notEmpty()` failed');\n\n    validator = validators.minLength(5);\n\n    expect(isValidatorInstance(validator));\n    expect(validator.getName()).toBe('minLength');\n    expect(typeof validator.getFunction()).toBe('function');\n    expect(validator.getArguments()).toEqual([5]);\n    expect(validator.getMessage()).toBe('The validator `minLength(5)` failed');\n\n    validator = validators.minLength(5, 'The minimum length is 5');\n\n    expect(isValidatorInstance(validator));\n    expect(validator.getName()).toBe('minLength');\n    expect(typeof validator.getFunction()).toBe('function');\n    expect(validator.getArguments()).toEqual([5]);\n    expect(validator.getMessage()).toBe('The minimum length is 5');\n\n    expect(() => validators.minLength()).toThrow(\n      \"A required parameter is missing to build the validator 'minLength'\"\n    );\n\n    expect(() => validators.minLength(5, 10)).toThrow(\n      \"When building a validator, if an extra parameter is specified, it must be a string representing the failed validation message (validator: 'minLength')\"\n    );\n\n    validator = validators.anyOf([1, 2, 3]);\n\n    expect(isValidatorInstance(validator));\n    expect(validator.getName()).toBe('anyOf');\n    expect(typeof validator.getFunction()).toBe('function');\n    expect(validator.getArguments()).toEqual([[1, 2, 3]]);\n    expect(validator.getMessage()).toBe('The validator `anyOf([1,2,3])` failed');\n\n    const missingValidator = validators.missing();\n    const minLengthValidator = validators.minLength(5);\n    validator = validators.either([missingValidator, minLengthValidator]);\n\n    expect(isValidatorInstance(validator));\n    expect(validator.getName()).toBe('either');\n    expect(typeof validator.getFunction()).toBe('function');\n    expect(validator.getArguments()).toEqual([[missingValidator, minLengthValidator]]);\n    expect(validator.getMessage()).toBe('The validator `either([missing(),minLength(5)])` failed');\n  });\n\n  test('Running built-in validators', async () => {\n    expect(validators.required().run(1)).toBe(true);\n    expect(validators.required().run(undefined)).toBe(false);\n\n    expect(validators.anyOf([1, 2, 3]).run(1)).toBe(true);\n    expect(validators.anyOf([1, 2, 3]).run(5)).toBe(false);\n\n    expect(validators.positive().run(1)).toBe(true);\n    expect(validators.positive().run(-1)).toBe(false);\n\n    expect(validators.lessThan(5).run(3)).toBe(true);\n    expect(validators.lessThan(5).run(7)).toBe(false);\n\n    expect(validators.range([5, 10]).run(7)).toBe(true);\n    expect(validators.range([5, 10]).run(3)).toBe(false);\n\n    expect(validators.notEmpty().run('abc')).toBe(true);\n    expect(validators.notEmpty().run('')).toBe(false);\n    expect(validators.notEmpty().run([1])).toBe(true);\n    expect(validators.notEmpty().run([])).toBe(false);\n\n    expect(validators.maxLength(3).run('abc')).toBe(true);\n    expect(validators.maxLength(3).run('abcd')).toBe(false);\n    expect(validators.maxLength(3).run([1, 2, 3])).toBe(true);\n    expect(validators.maxLength(3).run([1, 2, 3, 4])).toBe(false);\n\n    expect(validators.match(/b/).run('abc')).toBe(true);\n    expect(validators.match(/e/).run('abc')).toBe(false);\n\n    expect(validators.either([validators.missing(), validators.minLength(3)]).run(undefined)).toBe(\n      true\n    );\n    expect(validators.either([validators.missing(), validators.minLength(3)]).run('abc')).toBe(\n      true\n    );\n    expect(validators.either([validators.missing(), validators.minLength(3)]).run('ab')).toBe(\n      false\n    );\n\n    expect(validators.optional(validators.minLength(3)).run(undefined)).toBe(true);\n    expect(validators.optional(validators.minLength(3)).run('abc')).toBe(true);\n    expect(validators.optional(validators.minLength(3)).run('ab')).toBe(false);\n  });\n});\n"
  },
  {
    "path": "packages/component/src/validation/validator-builders.ts",
    "content": "import {Validator, ValidatorFunction} from './validator';\n\nconst validatorFunctions: {[name: string]: ValidatorFunction} = {\n  // Numbers\n\n  integer: (value) => value !== undefined && Number.isInteger(value),\n\n  positive: (value) => value !== undefined && value >= 0,\n\n  negative: (value) => value !== undefined && value < 0,\n\n  lessThan: (value, number) => value !== undefined && value < number,\n\n  lessThanOrEqual: (value, number) => value !== undefined && value <= number,\n\n  greaterThan: (value, number) => value !== undefined && value > number,\n\n  greaterThanOrEqual: (value, number) => value !== undefined && value >= number,\n\n  range: (value, [min, max]) => value !== undefined && value >= min && value <= max,\n\n  // Strings and arrays\n\n  notEmpty: (value) => value !== undefined && value.length > 0,\n\n  minLength: (value, minLength) => value !== undefined && value.length >= minLength,\n\n  maxLength: (value, maxLength) => value !== undefined && value.length <= maxLength,\n\n  rangeLength: (value, [minLength, maxLength]) =>\n    value !== undefined && value.length >= minLength && value.length <= maxLength,\n\n  // Strings\n\n  match: (value, pattern) => value !== undefined && pattern.test(value),\n\n  // Any values\n\n  required: (value) => value !== undefined,\n\n  missing: (value) => value === undefined,\n\n  anyOf: (value, array) => array.includes(value),\n\n  noneOf: (value, array) => !array.includes(value),\n\n  // Operators\n\n  either: (value, validators: Validator[]) => {\n    return validators.some((validator) => validator.run(value));\n  },\n\n  optional: (value, validators: Validator[] | Validator) => {\n    if (!Array.isArray(validators)) {\n      validators = [validators];\n    }\n\n    return value === undefined || validators.every((validator) => validator.run(value));\n  }\n};\n\nexport type ValidatorBuilder = (...args: any[]) => Validator;\n\nexport const validators: {[name: string]: ValidatorBuilder} = {};\n\nfor (const [name, func] of Object.entries(validatorFunctions)) {\n  validators[name] = (...args) => createValidator(name, func, args);\n}\n\nfunction createValidator(name: string, func: ValidatorFunction, args: any[]) {\n  const numberOfRequiredArguments = func.length - 1;\n  const validatorArguments = args.slice(0, numberOfRequiredArguments);\n\n  if (validatorArguments.length < numberOfRequiredArguments) {\n    throw new Error(`A required parameter is missing to build the validator '${name}'`);\n  }\n\n  const [message] = args.slice(numberOfRequiredArguments);\n\n  if (message !== undefined && typeof message !== 'string') {\n    throw new Error(\n      `When building a validator, if an extra parameter is specified, it must be a string representing the failed validation message (validator: '${name}')`\n    );\n  }\n\n  return new Validator(func, {name, arguments: validatorArguments, message});\n}\n"
  },
  {
    "path": "packages/component/src/validation/validator.test.ts",
    "content": "import {Validator, isValidatorInstance, runValidators} from './validator';\nimport {serialize} from '../serialization';\nimport {deserialize} from '../deserialization';\n\ndescribe('Validator', () => {\n  test('Creation', async () => {\n    const notEmpty = (value: string | any[]) => value.length > 0;\n    let validator = new Validator(notEmpty);\n\n    expect(isValidatorInstance(validator));\n    expect(validator.getName()).toBe('notEmpty');\n    expect(validator.getFunction()).toBe(notEmpty);\n    expect(validator.getArguments()).toEqual([]);\n    expect(validator.getMessage()).toBe('The validator `notEmpty()` failed');\n    expect(validator.getMessage({generateIfMissing: false})).toBeUndefined();\n\n    const greaterThan = (value: number, number: number) => value > number;\n    validator = new Validator(greaterThan, {arguments: [5]});\n\n    expect(isValidatorInstance(validator));\n    expect(validator.getName()).toBe('greaterThan');\n    expect(validator.getFunction()).toBe(greaterThan);\n    expect(validator.getArguments()).toEqual([5]);\n    expect(validator.getMessage()).toBe('The validator `greaterThan(5)` failed');\n    expect(validator.getMessage({generateIfMissing: false})).toBeUndefined();\n\n    const match = (value: string, pattern: RegExp) => pattern.test(value);\n    const regExp = /abc/gi;\n    validator = new Validator(match, {arguments: [regExp]});\n\n    expect(isValidatorInstance(validator));\n    expect(validator.getName()).toBe('match');\n    expect(validator.getFunction()).toBe(match);\n    expect(validator.getArguments()).toEqual([regExp]);\n    expect(validator.getMessage()).toBe('The validator `match(/abc/gi)` failed');\n    expect(validator.getMessage({generateIfMissing: false})).toBeUndefined();\n\n    const validatorFunction = (value: number, number: number) => value < number;\n\n    validator = new Validator(validatorFunction, {\n      name: 'lessThanOrEqual5',\n      message: 'The maximum value is 5'\n    });\n\n    expect(isValidatorInstance(validator));\n    expect(validator.getName()).toBe('lessThanOrEqual5');\n    expect(validator.getFunction()).toBe(validatorFunction);\n    expect(validator.getArguments()).toEqual([]);\n    expect(validator.getMessage()).toBe('The maximum value is 5');\n    expect(validator.getMessage({generateIfMissing: false})).toBe('The maximum value is 5');\n  });\n\n  test('Execution', async () => {\n    const validatorFunction = (value: number, number: number) => value > number;\n\n    const validator = new Validator(validatorFunction, {\n      name: 'greaterThan',\n      arguments: [5],\n      message: 'The value is not greater than 5'\n    });\n\n    expect(validator.run(7)).toBe(true);\n    expect(validator.run(3)).toBe(false);\n\n    expect(runValidators([validator], 7)).toEqual([]);\n    expect(runValidators([validator], 3)).toEqual([validator]);\n  });\n\n  test('Serialization', async () => {\n    const greaterThan = (value: number, number: number) => value > number;\n\n    const greaterThanValidator = new Validator(greaterThan, {\n      name: 'greaterThan',\n      arguments: [5],\n      message: 'The value is not greater than 5'\n    });\n\n    const serializedGreaterThanValidator = greaterThanValidator.serialize(serialize);\n\n    expect(serializedGreaterThanValidator).toStrictEqual({\n      __validator: {\n        name: 'greaterThan',\n        function: {__function: '(value, number) => value > number'},\n        arguments: [5],\n        message: 'The value is not greater than 5'\n      }\n    });\n\n    const optional = (value: any, validator: Validator) =>\n      value === undefined || validator.run(value);\n\n    const optionalValidator = new Validator(optional, {\n      name: 'optional',\n      arguments: [greaterThanValidator]\n    });\n\n    const serializedOptionalValidator = optionalValidator.serialize(serialize);\n\n    expect(serializedOptionalValidator).toStrictEqual({\n      __validator: {\n        name: 'optional',\n        function: {__function: '(value, validator) => value === undefined || validator.run(value)'},\n        arguments: [serializedGreaterThanValidator]\n      }\n    });\n  });\n\n  test('Deserialization', async () => {\n    const validator = Validator.recreate(\n      {\n        __validator: {\n          name: 'optional',\n          function: {\n            __function: '(value, validator) => value === undefined || validator.run(value)'\n          },\n          arguments: [\n            {\n              __validator: {\n                name: 'greaterThan',\n                function: {__function: '(value, number) => value > number'},\n                arguments: [5],\n                message: 'The value is not greater than 5'\n              }\n            }\n          ]\n        }\n      },\n      deserialize\n    );\n\n    expect(isValidatorInstance(validator));\n    expect(validator.getName()).toBe('optional');\n    expect(typeof validator.getFunction()).toBe('function');\n    expect(validator.getArguments().length).toBe(1);\n    expect(validator.getArguments()[0].getName()).toBe('greaterThan');\n    expect(typeof validator.getArguments()[0].getFunction()).toBe('function');\n    expect(validator.getArguments()[0].getArguments()).toEqual([5]);\n    expect(validator.getArguments()[0].getMessage()).toBe('The value is not greater than 5');\n    expect(validator.getMessage()).toBe('The validator `optional(greaterThan(5))` failed');\n  });\n});\n"
  },
  {
    "path": "packages/component/src/validation/validator.ts",
    "content": "import {hasOwnProperty, getFunctionName} from 'core-helpers';\n\nexport type ValidatorFunction = (value: any, ...args: any[]) => boolean;\n\ntype ValidatorOptions = {\n  name?: string;\n  arguments?: any[];\n  message?: string;\n};\n\n/**\n * A class to handle the validation of the component attributes.\n *\n * #### Usage\n *\n * You shouldn't have to create a `Validator` instance directly. Instead, when you define an attribute (using a decorator such as [`@attribute()`](https://layrjs.com/docs/v2/reference/component#attribute-decorator)), you can invoke some [built-in validator builders](https://layrjs.com/docs/v2/reference/validator#built-in-validator-builders) or specify your own [custom validation functions](https://layrjs.com/docs/v2/reference/validator#custom-validation-functions) that will be automatically transformed into `Validator` instances.\n *\n * **Example:**\n *\n * ```\n * // JS\n *\n * import {Component, attribute, validators} from '﹫layr/component';\n *\n * const {notEmpty, maxLength, integer, greaterThan} = validators;\n *\n * class Movie extends Component {\n *   // An attribute of type 'string' that cannot be empty or exceed 30 characters\n *   ﹫attribute('string', {validators: [notEmpty(), maxLength(30)]}) title;\n *\n *   // An attribute of type 'number' that must an integer greater than 0\n *   ﹫attribute('number', {validators: [integer(), greaterThan(0)]}) reference;\n *\n *   // An array attribute that can contain up to 5 non-empty strings\n *   ﹫attribute('string[]', {validators: [maxLength(5)], items: {validators: [notEmpty()]}})\n *   tags = [];\n * }\n * ```\n *\n * ```\n * // TS\n *\n * import {Component, attribute, validators} from '﹫layr/component';\n *\n * const {notEmpty, maxLength, integer, greaterThan} = validators;\n *\n * class Movie extends Component {\n *   // An attribute of type 'string' that cannot be empty or exceed 30 characters\n *   ﹫attribute('string', {validators: [notEmpty(), maxLength(30)]}) title!: string;\n *\n *   // An attribute of type 'number' that must an integer greater than 0\n *   ﹫attribute('number', {validators: [integer(), greaterThan(0)]}) reference!: number;\n *\n *   // An array attribute that can contain up to 5 non-empty strings\n *   ﹫attribute('string[]', {validators: [maxLength(5)], items: {validators: [notEmpty()]}})\n *   tags: string[] = [];\n * }\n * ```\n *\n * In case you want to access the `Validator` instances that were created under the hood, you can do the following:\n *\n * ```\n * const movie = new Movie({ ... });\n *\n * movie.getAttribute('title').getValueType().getValidators();\n * // => [notEmptyValidator, maxLengthValidator]\n *\n * movie.getAttribute('reference').getValueType().getValidators();\n * // => [integerValidator, greaterThanValidator]\n *\n * movie.getAttribute('tags').getValueType().getValidators();\n * // => [maxLengthValidator]\n *\n * movie.getAttribute('tags').getValueType().getItemType().getValidators();\n * // => [notEmptyValidator]\n * ```\n *\n * #### Built-In Validator Builders\n *\n * Layr provides a number of validator builders that can be used when you define your component attributes. See an [example of use](https://layrjs.com/docs/v2/reference/validator#usage) above.\n *\n * ##### Numbers\n *\n * The following validator builders can be used to validate numbers:\n *\n * * `integer()`: Ensures that a number is an integer.\n * * `positive()`: Ensures that a number is greater than or equal to 0.\n * * `negative()`: Ensures that a number is less than 0.\n * * `lessThan(value)`: Ensures that a number is less than the specified value.\n * * `lessThanOrEqual(value)`: Ensures that a number is less than or equal to the specified value.\n * * `greaterThan(value)`: Ensures that a number is greater than the specified value.\n * * `greaterThanOrEqual(value)`: Ensures that a number is greater than or equal to the specified value.\n * * `range([min, max])`: Ensures that a number is in the specified inclusive range.\n * * `anyOf(arrayOfNumbers)`: Ensures that a number is any of the specified numbers.\n * * `noneOf(arrayOfNumbers)`: Ensures that a number is none of the specified numbers.\n *\n * ##### Strings\n *\n * The following validator builders can be used to validate strings:\n *\n * * `notEmpty()`: Ensures that a string is not empty.\n * * `minLength(value)`: Ensures that a string has at least the specified number of characters.\n * * `maxLength(value)`: Ensures that a string doesn't exceed the specified number of characters.\n * * `rangeLength([min, max])`: Ensures that the length of a string is in the specified inclusive range.\n * * `match(regExp)`: Ensures that a string matches the specified [regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions).\n * * `anyOf(arrayOfStrings)`: Ensures that a string is any of the specified strings.\n * * `noneOf(arrayOfStrings)`: Ensures that a string is none of the specified strings.\n *\n * ##### Arrays\n *\n * The following validator builders can be used to validate arrays:\n *\n * * `notEmpty()`: Ensures that an array is not empty.\n * * `minLength(value)`: Ensures that an array has at least the specified number of items.\n * * `maxLength(value)`: Ensures that an array doesn't exceed the specified number of items.\n * * `rangeLength([min, max])`: Ensures that the length of an array is in the specified inclusive range.\n *\n * ##### Any Type\n *\n * The following validator builder can be used to validate any type of values:\n *\n * * `required()`: Ensures that a value is not undefined.\n * * `missing()`: Ensures that a value is undefined.\n *\n * ##### Validator Operators\n *\n * You can compose several validators using some validator operators:\n *\n * * `either(arrayOfValidators)`: Performs a logical **OR** operation on an array of validators.\n * * `optional(validatorOrArrayOfValidators)`: If a value is is not undefined, ensures that it satisfies the specified validators (can be a single validator or an array of validators).\n *\n * ##### Custom Failed Validation Message\n *\n * You can pass an additional parameter to all the built-in validators builder to customize the message of the error that is thrown in case of failed validation.\n *\n * **Example:**\n *\n * ```\n * maxLength(16, 'A username cannot exceed 16 characters');\n * ```\n *\n * #### Custom Validation Functions\n *\n * In addition to the [built-in validator builders](https://layrjs.com/docs/v2/reference/validator#built-in-validator-builders), you can validate your component attributes with your own custom validation functions.\n *\n * A custom validation function takes a value as first parameter and returns a boolean indicating whether the validation has succeeded or not.\n *\n * **Example:**\n *\n * ```\n * // JS\n *\n * import {Component, attribute} from '﹫layr/component';\n *\n * class OddNumber extends Component {\n *   // Ensures that the value is an odd number\n *   ﹫attribute('number', {validators: [(value) => value % 2 === 1]}) value;\n * }\n * ```\n *\n * ```\n * // TS\n *\n * import {Component, attribute} from '﹫layr/component';\n *\n * class OddNumber extends Component {\n *   // Ensures that the value is an odd number\n *   ﹫attribute('number', {validators: [(value) => value % 2 === 1]}) value!: number;\n * }\n * ```\n */\nexport class Validator {\n  _function: ValidatorFunction;\n  _name: string;\n  _arguments: any[];\n  _message: string | undefined;\n\n  constructor(func: ValidatorFunction, options: ValidatorOptions = {}) {\n    let {name, arguments: args = [], message} = options;\n\n    if (name === undefined) {\n      name = getFunctionName(func) || 'anonymous';\n    }\n\n    this._function = func;\n    this._name = name;\n    this._arguments = args;\n    this._message = message;\n  }\n\n  /**\n   * Returns the function associated to the validator.\n   *\n   * @returns A function.\n   *\n   * @example\n   * ```\n   * maxLength(8).getFunction();\n   * // => function (value, maxLength) { return value.length <= maxLength; }\n   * ```\n   *\n   * @category Methods\n   */\n  getFunction() {\n    return this._function;\n  }\n\n  /**\n   * Returns the name of the validator.\n   *\n   * @returns A string.\n   *\n   * @example\n   * ```\n   * maxLength(8).getName(); // => 'maxLength'\n   * ```\n   *\n   * @category Methods\n   */\n  getName() {\n    return this._name;\n  }\n\n  /**\n   * Returns the arguments of the validator.\n   *\n   * @returns An array of values of any type.\n   *\n   * @example\n   * ```\n   * maxLength(8).getArguments(); // => [8]\n   * ```\n   *\n   * @category Methods\n   */\n  getArguments() {\n    return this._arguments;\n  }\n\n  getSignature(): string {\n    return `${this.getName()}(${stringifyArguments(this.getArguments())})`;\n  }\n\n  /**\n   * Returns the message of the error that is thrown in case of failed validation.\n   *\n   * @returns A string.\n   *\n   * @example\n   * ```\n   * maxLength(8).getMessage(); // => 'The validator maxLength(8) failed'\n   * ```\n   *\n   * @category Methods\n   */\n  getMessage({generateIfMissing = true}: {generateIfMissing?: boolean} = {}) {\n    let message = this._message;\n\n    if (message === undefined && generateIfMissing) {\n      message = `The validator \\`${this.getSignature()}\\` failed`;\n    }\n\n    return message;\n  }\n\n  /**\n   * Runs the validator against the specified value.\n   *\n   * @returns `true` if the validation has succeeded, `false` otherwise.\n   *\n   * @example\n   * ```\n   * maxLength(8).run('1234567'); // => true\n   * maxLength(8).run('12345678'); // => true\n   * maxLength(8).run('123456789'); // => false\n   * ```\n   *\n   * @category Methods\n   */\n  run(value: any) {\n    return this.getFunction()(value, ...this.getArguments());\n  }\n\n  serialize(serializer: Function) {\n    let serializedValidator: any = {\n      name: this.getName(),\n      function: this.getFunction()\n    };\n\n    const args = this.getArguments();\n\n    if (args.length > 0) {\n      serializedValidator.arguments = args;\n    }\n\n    if (this._message !== undefined) {\n      serializedValidator.message = this._message;\n    }\n\n    serializedValidator = {\n      __validator: serializer(serializedValidator, {serializeFunctions: true})\n    };\n\n    return serializedValidator;\n  }\n\n  static recreate(serializedValidator: any, deserializer: Function) {\n    const {\n      name,\n      function: func,\n      arguments: args,\n      message\n    } = deserializer(serializedValidator.__validator, {deserializeFunctions: true});\n\n    const validator = new this(func, {name, arguments: args, message});\n\n    return validator;\n  }\n\n  static isValidator(value: any): value is Validator {\n    return isValidatorInstance(value);\n  }\n}\n\nexport function isValidatorInstance(value: any): value is Validator {\n  return typeof value?.constructor?.isValidator === 'function';\n}\n\nexport function isSerializedValidator(object: object) {\n  return object !== undefined && hasOwnProperty(object, '__validator');\n}\n\nexport function runValidators(validators: Validator[], value: any) {\n  const failedValidators: Validator[] = [];\n\n  for (const validator of validators) {\n    if (!validator.run(value)) {\n      failedValidators.push(validator);\n    }\n  }\n\n  return failedValidators;\n}\n\nfunction stringifyArguments(args: any[]) {\n  let string = JSON.stringify(args, (_key, value) => {\n    if (value instanceof RegExp) {\n      return `__regExp(${value.toString()})regExp__`;\n    }\n\n    if (value instanceof Validator) {\n      return `__validator(${value.getSignature()})validator__`;\n    }\n\n    return value;\n  });\n\n  // Fix RegExps\n  string = string.replace(/\"__regExp\\(/g, '');\n  string = string.replace(/\\)regExp__\"/g, '');\n\n  // Fix validator signatures\n  string = string.replace(/\"__validator\\(/g, '');\n  string = string.replace(/\\)validator__\"/g, '');\n\n  // Remove the array brackets\n  string = string.slice(1, -1);\n\n  return string;\n}\n"
  },
  {
    "path": "packages/component/tsconfig.json",
    "content": "{\n  \"extends\": \"@mvila/tsconfig\",\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/component-client/README.md",
    "content": "# @layr/component-client\n\nConsumes Layr components served by @layr/component-server.\n\n## Installation\n\n```\nnpm install @layr/component-client\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/component-client/package.json",
    "content": "{\n  \"name\": \"@layr/component-client\",\n  \"version\": \"2.0.86\",\n  \"description\": \"Consumes Layr components served by @layr/component-server\",\n  \"keywords\": [\n    \"layr\",\n    \"component\",\n    \"client\"\n  ],\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/layrjs/layr/tree/master/packages/component-client\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"dist/node-cjs/index.js\",\n  \"module\": \"dist/node-esm/index.js\",\n  \"engines\": {\n    \"node\": \">=16.0.0\"\n  },\n  \"scripts\": {\n    \"build\": \"dev-tools build:ts-library\",\n    \"prepare\": \"npm run build\",\n    \"test\": \"dev-tools test:ts-library\",\n    \"publish:package\": \"dev-tools publish:package\",\n    \"update\": \"dev-tools update:dependencies\"\n  },\n  \"dependencies\": {\n    \"@layr/component\": \"^2.0.51\",\n    \"@layr/component-server\": \"^2.0.70\",\n    \"core-helpers\": \"^1.0.8\",\n    \"debug\": \"^4.3.4\",\n    \"lodash\": \"^4.17.21\",\n    \"microbatcher\": \"^2.0.8\",\n    \"possibly-async\": \"^1.0.7\",\n    \"tslib\": \"^2.4.1\"\n  },\n  \"devDependencies\": {\n    \"@mvila/dev-tools\": \"^1.3.1\",\n    \"@mvila/tsconfig\": \"^1.0.6\",\n    \"@types/debug\": \"^4.1.7\",\n    \"@types/jest\": \"^29.2.5\",\n    \"@types/lodash\": \"^4.14.191\"\n  }\n}\n"
  },
  {
    "path": "packages/component-client/src/component-client.test.ts",
    "content": "import {Component, isComponentClass} from '@layr/component';\nimport type {ComponentServerLike} from '@layr/component-server';\nimport isEqual from 'lodash/isEqual';\n\nimport {ComponentClient} from './component-client';\n\ndescribe('ComponentClient', () => {\n  const server: ComponentServerLike = {\n    receive({query, components, version: clientVersion}) {\n      const serverVersion = 1;\n\n      if (clientVersion !== serverVersion) {\n        throw Object.assign(\n          new Error(\n            `The component client version (${clientVersion}) doesn't match the component server version (${serverVersion})`\n          ),\n          {code: 'COMPONENT_CLIENT_VERSION_DOES_NOT_MATCH_COMPONENT_SERVER_VERSION'}\n        );\n      }\n\n      // client.getComponents()\n      if (\n        isEqual({query, components}, {query: {'introspect=>': {'()': []}}, components: undefined})\n      ) {\n        return {\n          result: {\n            component: {\n              name: 'Backend',\n              providedComponents: [\n                {\n                  name: 'Session',\n                  properties: [\n                    {\n                      name: 'token',\n                      type: 'Attribute',\n                      valueType: 'string?',\n                      value: {__undefined: true},\n                      exposure: {get: true, set: true}\n                    }\n                  ]\n                },\n                {\n                  name: 'Movie',\n                  mixins: ['Storable'],\n                  properties: [{name: 'find', type: 'Method', exposure: {call: true}}],\n                  prototype: {\n                    properties: [\n                      {\n                        name: 'id',\n                        type: 'PrimaryIdentifierAttribute',\n                        valueType: 'string',\n                        default: {\n                          __function: 'function () {\\nreturn this.constructor.generateId();\\n}'\n                        },\n                        exposure: {get: true, set: true}\n                      },\n                      {\n                        name: 'slug',\n                        type: 'SecondaryIdentifierAttribute',\n                        valueType: 'string',\n                        exposure: {get: true, set: true}\n                      },\n                      {\n                        name: 'title',\n                        type: 'Attribute',\n                        valueType: 'string',\n                        default: {__function: \"function () {\\nreturn '';\\n}\"},\n                        validators: [\n                          {\n                            __validator: {\n                              name: 'notEmpty',\n                              function: {__function: '(value) => value.length > 0'}\n                            }\n                          }\n                        ],\n                        exposure: {get: true, set: true}\n                      },\n                      {\n                        name: 'isPlaying',\n                        type: 'Attribute',\n                        valueType: 'boolean',\n                        exposure: {get: true}\n                      },\n                      {name: 'play', type: 'Method', exposure: {call: true}},\n                      {name: 'validateTitle', type: 'Method', exposure: {call: true}}\n                    ]\n                  },\n                  consumedComponents: ['Session']\n                }\n              ]\n            }\n          }\n        };\n      }\n\n      // Movie.find() without Session's token\n      if (\n        isEqual(\n          {query, components},\n          {\n            query: {\n              '<=': {__component: 'typeof Movie'},\n              'find=>': {'()': []}\n            },\n            components: [{__component: 'typeof Session', token: {__undefined: true}}]\n          }\n        )\n      ) {\n        return {\n          result: {__error: 'Access denied'}\n        };\n      }\n\n      // Movie.find() with Session's token\n      if (\n        isEqual(\n          {query, components},\n          {\n            query: {\n              '<=': {__component: 'typeof Movie'},\n              'find=>': {'()': []}\n            },\n            components: [{__component: 'typeof Session', token: 'abc123'}]\n          }\n        )\n      ) {\n        return {\n          result: [\n            {\n              __component: 'Movie',\n              id: 'movie1',\n              slug: 'inception',\n              title: 'Inception',\n              isPlaying: false\n            },\n            {\n              __component: 'Movie',\n              id: 'movie2',\n              slug: 'the-matrix',\n              title: 'The Matrix',\n              isPlaying: false\n            }\n          ]\n        };\n      }\n\n      // Movie.find({limit: 1})\n      if (\n        isEqual(\n          {query, components},\n          {\n            query: {\n              '<=': {__component: 'typeof Movie'},\n              'find=>': {'()': [{limit: 1}]}\n            },\n            components: [{__component: 'typeof Session', token: 'abc123'}]\n          }\n        )\n      ) {\n        return {\n          result: [\n            {\n              __component: 'Movie',\n              id: 'movie1',\n              slug: 'inception',\n              title: 'Inception',\n              isPlaying: false\n            }\n          ]\n        };\n      }\n\n      // movie.play()\n      if (\n        isEqual(\n          {query, components},\n          {\n            query: {'<=': {__component: 'Movie', id: 'movie1'}, 'play=>': {'()': []}},\n            components: [{__component: 'typeof Session', token: 'abc123'}]\n          }\n        )\n      ) {\n        return {\n          result: {__component: 'Movie', id: 'movie1', isPlaying: true}\n        };\n      }\n\n      // movie.validateTitle('')\n      if (\n        isEqual(\n          {query, components},\n          {\n            query: {\n              '<=': {__component: 'Movie', id: 'movie1', title: ''},\n              'validateTitle=>': {'()': []}\n            },\n            components: [{__component: 'typeof Session', token: 'abc123'}]\n          }\n        )\n      ) {\n        return {\n          result: false\n        };\n      }\n\n      // movie.validateTitle('Inception 2')\n      if (\n        isEqual(\n          {query, components},\n          {\n            query: {\n              '<=': {__component: 'Movie', id: 'movie1', title: 'Inception 2'},\n              'validateTitle=>': {'()': []}\n            },\n            components: [{__component: 'typeof Session', token: 'abc123'}]\n          }\n        )\n      ) {\n        return {\n          result: true\n        };\n      }\n\n      // [movie1.play(), movie2.play()]\n      if (\n        isEqual(\n          {query, components},\n          {\n            query: {\n              '||': [\n                {'<=': {__component: 'Movie', id: 'movie1'}, 'play=>': {'()': []}},\n                {'<=': {__component: 'Movie', id: 'movie2'}, 'play=>': {'()': []}}\n              ]\n            },\n            components: [{__component: 'typeof Session', token: 'abc123'}]\n          }\n        )\n      ) {\n        return {\n          result: [\n            {__component: 'Movie', id: 'movie1', isPlaying: true},\n            {__component: 'Movie', id: 'movie2', isPlaying: true}\n          ]\n        };\n      }\n\n      throw new Error(\n        `Received an unknown request (query: ${JSON.stringify(query)}, components: ${JSON.stringify(\n          components\n        )})`\n      );\n    }\n  };\n\n  const Storable = (Base = Component) => {\n    const _Storable = class extends Base {\n      static isStorable() {}\n      isStorable() {}\n    };\n\n    Object.defineProperty(_Storable, '__mixin', {value: 'Storable'});\n\n    return _Storable;\n  };\n\n  test('Getting component', async () => {\n    let client = new ComponentClient(server);\n\n    expect(() => client.getComponent()).toThrow(\n      \"The component client version (undefined) doesn't match the component server version (1)\"\n    );\n\n    client = new ComponentClient(server, {version: 1, mixins: [Storable]});\n\n    const Backend = client.getComponent() as typeof Component;\n\n    expect(isComponentClass(Backend)).toBe(true);\n    expect(Backend.getComponentName()).toBe('Backend');\n\n    const Session = Backend.getProvidedComponent('Session')!;\n\n    expect(isComponentClass(Session)).toBe(true);\n    expect(Session.getComponentName()).toBe('Session');\n\n    let attribute = Session.getAttribute('token');\n\n    expect(attribute.getValueType().toString()).toBe('string?');\n    expect(attribute.getExposure()).toEqual({get: true, set: true});\n    expect(attribute.getValue()).toBeUndefined();\n\n    const Movie = Backend.getProvidedComponent('Movie')!;\n\n    expect(isComponentClass(Movie)).toBe(true);\n    expect(Movie.getComponentName()).toBe('Movie');\n    expect(Movie.getConsumedComponent('Session')).toBe(Session);\n\n    let method = Movie.getMethod('find');\n\n    expect(method.getExposure()).toEqual({call: true});\n\n    attribute = Movie.prototype.getPrimaryIdentifierAttribute();\n\n    expect(attribute.getName()).toBe('id');\n    expect(attribute.getValueType().toString()).toBe('string');\n    expect(typeof attribute.getDefault()).toBe('function');\n    expect(attribute.getExposure()).toEqual({get: true, set: true});\n\n    attribute = Movie.prototype.getSecondaryIdentifierAttribute('slug');\n\n    expect(attribute.getValueType().toString()).toBe('string');\n    expect(attribute.getDefault()).toBeUndefined();\n    expect(attribute.getExposure()).toEqual({get: true, set: true});\n\n    attribute = Movie.prototype.getAttribute('title');\n\n    expect(attribute.getValueType().toString()).toBe('string');\n    expect(attribute.evaluateDefault()).toBe('');\n    expect(attribute.getValueType().getValidators()).toHaveLength(1);\n    expect(attribute.getExposure()).toEqual({get: true, set: true});\n\n    attribute = Movie.prototype.getAttribute('isPlaying');\n\n    expect(attribute.getValueType().toString()).toBe('boolean');\n    expect(attribute.evaluateDefault()).toBe(undefined);\n    expect(attribute.getExposure()).toEqual({get: true});\n    expect(attribute.isControlled()).toBe(true);\n\n    method = Movie.prototype.getMethod('play');\n\n    expect(method.getExposure()).toEqual({call: true});\n\n    method = Movie.prototype.getMethod('validateTitle');\n\n    expect(method.getExposure()).toEqual({call: true});\n\n    expect(typeof (Movie as any).isStorable).toBe('function');\n  });\n\n  describe('Invoking methods', () => {\n    class BaseSession extends Component {\n      static token?: string;\n    }\n\n    class BaseMovie extends Component {\n      static Session: typeof BaseSession;\n\n      // @ts-ignore\n      static find({limit}: {limit?: number} = {}): BaseMovie[] {}\n\n      id!: string;\n      slug!: string;\n      title = '';\n      isPlaying = false;\n      play() {}\n      // @ts-ignore\n      validateTitle(): boolean {}\n    }\n\n    class BaseBackend extends Component {\n      static Session: typeof BaseSession;\n      static Movie: typeof BaseMovie;\n    }\n\n    test('One by one', async () => {\n      const client = new ComponentClient(server, {version: 1, mixins: [Storable]});\n\n      const {Movie, Session} = client.getComponent() as typeof BaseBackend;\n\n      expect(() => Movie.find()).toThrow('Access denied'); // The token is missing\n\n      Session.token = 'abc123';\n\n      let movies = Movie.find();\n\n      expect(movies).toHaveLength(2);\n      expect(movies[0]).toBeInstanceOf(Movie);\n      expect(movies[0].id).toBe('movie1');\n      expect(movies[0].slug).toBe('inception');\n      expect(movies[0].title).toBe('Inception');\n      expect(movies[1]).toBeInstanceOf(Movie);\n      expect(movies[1].id).toBe('movie2');\n      expect(movies[1].slug).toBe('the-matrix');\n      expect(movies[1].title).toBe('The Matrix');\n\n      movies = Movie.find({limit: 1});\n\n      expect(movies).toHaveLength(1);\n      expect(movies[0]).toBeInstanceOf(Movie);\n      expect(movies[0].id).toBe('movie1');\n      expect(movies[0].slug).toBe('inception');\n      expect(movies[0].title).toBe('Inception');\n\n      const movie = movies[0];\n\n      movie.play();\n\n      expect(movie.isPlaying).toBe(true);\n\n      movie.title = '';\n\n      expect(movie.validateTitle()).toBe(false);\n\n      movie.title = 'Inception 2';\n\n      expect(movie.validateTitle()).toBe(true);\n    });\n\n    test('In batch mode', async () => {\n      const client = new ComponentClient(server, {version: 1, mixins: [Storable], batchable: true});\n\n      const {Movie, Session} = (await client.getComponent()) as typeof BaseBackend;\n\n      Session.token = 'abc123';\n\n      const movies = await Movie.find();\n\n      expect(movies).toHaveLength(2);\n      expect(movies[0]).toBeInstanceOf(Movie);\n      expect(movies[0].id).toBe('movie1');\n      expect(movies[0].slug).toBe('inception');\n      expect(movies[0].title).toBe('Inception');\n      expect(movies[1]).toBeInstanceOf(Movie);\n      expect(movies[1].id).toBe('movie2');\n      expect(movies[1].slug).toBe('the-matrix');\n      expect(movies[1].title).toBe('The Matrix');\n\n      await Promise.all([movies[0].play(), movies[1].play()]);\n\n      expect(movies[0].isPlaying).toBe(true);\n      expect(movies[1].isPlaying).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/component-client/src/component-client.ts",
    "content": "import {\n  Component,\n  ComponentSet,\n  Attribute,\n  serialize,\n  deserialize,\n  ensureComponentClass,\n  ComponentMixin,\n  assertIsComponentMixin,\n  IntrospectedComponent\n} from '@layr/component';\nimport type {ComponentServerLike} from '@layr/component-server';\nimport {Microbatcher, Operation} from 'microbatcher';\nimport {getTypeOf, PlainObject} from 'core-helpers';\nimport {possiblyAsync} from 'possibly-async';\nimport debugModule from 'debug';\n\nconst debug = debugModule('layr:component-client');\n// To display the debug log, set this environment:\n// DEBUG=layr:component-client DEBUG_DEPTH=5\n\nimport {isComponentClientInstance} from './utilities';\n\ninterface SendOperation extends Operation {\n  params: Parameters<ComponentClient['_sendOne']>;\n  resolve: (value: ReturnType<ComponentClient['_sendOne']>) => void;\n}\n\nexport type ComponentClientOptions = {\n  version?: number;\n  mixins?: ComponentMixin[];\n  introspection?: IntrospectedComponent;\n  batchable?: boolean;\n};\n\n/**\n * A base class allowing to access a root [`Component`](https://layrjs.com/docs/v2/reference/component) that is served by a [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server).\n *\n * Typically, instead of using this class, you would use a subclass such as [`ComponentHTTPClient`](https://layrjs.com/docs/v2/reference/component-http-client).\n */\nexport class ComponentClient {\n  _componentServer: ComponentServerLike;\n  _version: number | undefined;\n  _mixins: ComponentMixin[] | undefined;\n  _introspection: IntrospectedComponent | undefined;\n  _sendBatcher: Microbatcher<SendOperation> | undefined;\n\n  /**\n   * Creates a component client.\n   *\n   * @param componentServer The [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server) to connect to.\n   * @param [options.version] A number specifying the expected version of the component server (default: `undefined`). If a version is specified, an error is thrown when a request is sent and the component server has a different version. The thrown error is a JavaScript `Error` instance with a `code` attribute set to `'COMPONENT_CLIENT_VERSION_DOES_NOT_MATCH_COMPONENT_SERVER_VERSION'`.\n   * @param [options.mixins] An array of the component mixins (e.g., [`Storable`](https://layrjs.com/docs/v2/reference/storable)) to use when constructing the components exposed by the component server (default: `[]`).\n   *\n   * @returns A `ComponentClient` instance.\n   *\n   * @example\n   * ```\n   * // JS\n   *\n   * import {Component, attribute, expose} from '﹫layr/component';\n   * import {ComponentClient} from '﹫layr/component-client';\n   * import {ComponentServer} from '﹫layr/component-server';\n   *\n   * class Movie extends Component {\n   *   ﹫expose({get: true, set: true}) ﹫attribute('string') title;\n   * }\n   *\n   * const server = new ComponentServer(Movie);\n   * const client = new ComponentClient(server);\n   *\n   * const RemoteMovie = client.getComponent();\n   * ```\n   *\n   * @example\n   * ```\n   * // TS\n   *\n   * import {Component, attribute, expose} from '﹫layr/component';\n   * import {ComponentClient} from '﹫layr/component-client';\n   * import {ComponentServer} from '﹫layr/component-server';\n   *\n   * class Movie extends Component {\n   *   ﹫expose({get: true, set: true}) ﹫attribute('string') title!: string;\n   * }\n   *\n   * const server = new ComponentServer(Movie);\n   * const client = new ComponentClient(server);\n   *\n   * const RemoteMovie = client.getComponent() as typeof Movie;\n   * ```\n   *\n   * @category Creation\n   */\n  constructor(componentServer: ComponentServerLike, options: ComponentClientOptions = {}) {\n    const {version, mixins, introspection, batchable = false} = options;\n\n    if (typeof componentServer?.receive !== 'function') {\n      throw new Error(\n        `Expected a component server, but received a value of type '${getTypeOf(componentServer)}'`\n      );\n    }\n\n    if (mixins !== undefined) {\n      for (const mixin of mixins) {\n        assertIsComponentMixin(mixin);\n      }\n    }\n\n    this._componentServer = componentServer;\n    this._version = version;\n    this._mixins = mixins;\n    this._introspection = introspection;\n\n    if (batchable) {\n      this._sendBatcher = new Microbatcher(this._sendMany.bind(this));\n    }\n  }\n\n  _component!: typeof Component;\n\n  /**\n   * Gets the component that is served by the component server.\n   *\n   * @returns A [`Component`](https://layrjs.com/docs/v2/reference/component) class.\n   *\n   * @examplelink See [`constructor`'s example](https://layrjs.com/docs/v2/reference/component-client#constructor).\n   *\n   * @category Getting the Served Component\n   * @possiblyasync\n   */\n  getComponent() {\n    if (this._component === undefined) {\n      return possiblyAsync(this._createComponent(), (component) => {\n        this._component = component;\n        return component;\n      });\n    }\n\n    return this._component;\n  }\n\n  _createComponent() {\n    return possiblyAsync(this._introspectComponentServer(), (introspectedComponentServer) => {\n      const methodBuilder = (name: string) => this._createMethodProxy(name);\n\n      return Component.unintrospect(introspectedComponentServer.component, {\n        mixins: this._mixins,\n        methodBuilder\n      });\n    });\n  }\n\n  _createMethodProxy(name: string) {\n    const componentClient = this;\n\n    return function (this: typeof Component | Component, ...args: any[]) {\n      const query = {\n        '<=': this,\n        [`${name}=>`]: {'()': args}\n      };\n\n      const rootComponent = ensureComponentClass(this);\n\n      return componentClient.send(query, {rootComponent});\n    };\n  }\n\n  _introspectedComponentServer!: PlainObject;\n\n  _introspectComponentServer() {\n    if (this._introspectedComponentServer !== undefined) {\n      return this._introspectedComponentServer;\n    }\n\n    if (this._introspection !== undefined) {\n      this._introspectedComponentServer = {component: this._introspection};\n      return this._introspectedComponentServer;\n    }\n\n    const query = {'introspect=>': {'()': []}};\n\n    return possiblyAsync(this.send(query), (introspectedComponentServer) => {\n      this._introspectedComponentServer = introspectedComponentServer;\n      return introspectedComponentServer;\n    });\n  }\n\n  send(query: PlainObject, options: {rootComponent?: typeof Component} = {}): any {\n    if (this._sendBatcher !== undefined) {\n      return this._sendBatcher.batch(query, options);\n    }\n\n    return this._sendOne(query, options);\n  }\n\n  _sendOne(query: PlainObject, options: {rootComponent?: typeof Component}): any {\n    const {rootComponent} = options;\n\n    const {serializedQuery, serializedComponents} = this._serializeQuery(query);\n\n    debugRequest({serializedQuery, serializedComponents});\n\n    return possiblyAsync(\n      this._componentServer.receive({\n        query: serializedQuery,\n        ...(serializedComponents && {components: serializedComponents}),\n        version: this._version\n      }),\n      ({result: serializedResult, components: serializedComponents}) => {\n        debugResponse({serializedResult, serializedComponents});\n\n        const errorHandler = function (error: Error) {\n          throw error;\n        };\n\n        return possiblyAsync(\n          deserialize(serializedComponents, {\n            rootComponent,\n            deserializeFunctions: true,\n            errorHandler,\n            source: 'server'\n          }),\n          () => {\n            return deserialize(serializedResult, {\n              rootComponent,\n              deserializeFunctions: true,\n              errorHandler,\n              source: 'server'\n            });\n          }\n        );\n      }\n    );\n  }\n\n  async _sendMany(operations: SendOperation[]) {\n    if (operations.length === 1) {\n      const operation = operations[0];\n\n      try {\n        operation.resolve(await this._sendOne(...operation.params));\n      } catch (error) {\n        operation.reject(error);\n      }\n\n      return;\n    }\n\n    const queries = {'||': operations.map(({params: [query]}) => query)};\n\n    const {serializedQuery, serializedComponents} = this._serializeQuery(queries);\n\n    debugRequests({serializedQuery, serializedComponents});\n\n    const serializedResponse = await this._componentServer.receive({\n      query: serializedQuery,\n      ...(serializedComponents && {components: serializedComponents}),\n      version: this._version\n    });\n\n    debugResponses({\n      serializedResult: serializedResponse.result,\n      serializedComponents: serializedResponse.components\n    });\n\n    const errorHandler = function (error: Error) {\n      throw error;\n    };\n\n    const firstRootComponent = operations[0].params[1].rootComponent;\n\n    await deserialize(serializedResponse.components, {\n      rootComponent: firstRootComponent,\n      deserializeFunctions: true,\n      errorHandler,\n      source: 'server'\n    });\n\n    for (let index = 0; index < operations.length; index++) {\n      const operation = operations[index];\n      const serializedResult = (serializedResponse.result as unknown[])[index];\n\n      try {\n        const result = await deserialize(serializedResult, {\n          rootComponent: operation.params[1].rootComponent,\n          deserializeFunctions: true,\n          errorHandler,\n          source: 'server'\n        });\n\n        operation.resolve(result);\n      } catch (error) {\n        operation.reject(error);\n      }\n    }\n  }\n\n  _serializeQuery(query: PlainObject) {\n    const componentDependencies: ComponentSet = new Set();\n\n    const attributeFilter = function (this: typeof Component | Component, attribute: Attribute) {\n      // Exclude properties that cannot be set in the remote components\n\n      const remoteComponent = this.getRemoteComponent();\n\n      if (remoteComponent === undefined) {\n        return false;\n      }\n\n      const attributeName = attribute.getName();\n      const remoteAttribute = remoteComponent.hasAttribute(attributeName)\n        ? remoteComponent.getAttribute(attributeName)\n        : undefined;\n\n      if (remoteAttribute === undefined) {\n        return false;\n      }\n\n      return remoteAttribute.operationIsAllowed('set') as boolean;\n    };\n\n    const serializedQuery: PlainObject = serialize(query, {\n      componentDependencies,\n      attributeFilter,\n      target: 'server'\n    });\n\n    let serializedComponentDependencies: PlainObject[] | undefined;\n    const handledComponentDependencies: ComponentSet = new Set();\n\n    const serializeComponentDependencies = function (componentDependencies: ComponentSet) {\n      if (componentDependencies.size === 0) {\n        return;\n      }\n\n      const additionalComponentDependency: ComponentSet = new Set();\n\n      for (const componentDependency of componentDependencies.values()) {\n        if (handledComponentDependencies.has(componentDependency)) {\n          continue;\n        }\n\n        const serializedComponentDependency = componentDependency.serialize({\n          componentDependencies: additionalComponentDependency,\n          ignoreEmptyComponents: true,\n          attributeFilter,\n          target: 'server'\n        });\n\n        if (serializedComponentDependency !== undefined) {\n          if (serializedComponentDependencies === undefined) {\n            serializedComponentDependencies = [];\n          }\n\n          serializedComponentDependencies.push(serializedComponentDependency);\n        }\n\n        handledComponentDependencies.add(componentDependency);\n      }\n\n      serializeComponentDependencies(additionalComponentDependency);\n    };\n\n    serializeComponentDependencies(componentDependencies);\n\n    return {serializedQuery, serializedComponents: serializedComponentDependencies};\n  }\n\n  static isComponentClient(value: any): value is ComponentClient {\n    return isComponentClientInstance(value);\n  }\n}\n\nfunction debugRequest({\n  serializedQuery,\n  serializedComponents\n}: {\n  serializedQuery: PlainObject;\n  serializedComponents: PlainObject[] | undefined;\n}) {\n  let message = 'Sending query: %o';\n  const values = [serializedQuery];\n\n  if (serializedComponents !== undefined) {\n    message += ' (components: %o)';\n    values.push(serializedComponents);\n  }\n\n  debug(message, ...values);\n}\n\nfunction debugResponse({\n  serializedResult,\n  serializedComponents\n}: {\n  serializedResult: unknown;\n  serializedComponents: PlainObject[] | undefined;\n}) {\n  let message = 'Result received: %o';\n  const values = [serializedResult];\n\n  if (serializedComponents !== undefined) {\n    message += ' (components: %o)';\n    values.push(serializedComponents);\n  }\n\n  debug(message, ...values);\n}\n\nfunction debugRequests({\n  serializedQuery,\n  serializedComponents\n}: {\n  serializedQuery: PlainObject;\n  serializedComponents: PlainObject[] | undefined;\n}) {\n  let message = 'Sending queries: %o';\n  const values = [serializedQuery];\n\n  if (serializedComponents !== undefined) {\n    message += ' (components: %o)';\n    values.push(serializedComponents);\n  }\n\n  debug(message, ...values);\n}\n\nfunction debugResponses({\n  serializedResult,\n  serializedComponents\n}: {\n  serializedResult: unknown;\n  serializedComponents: PlainObject[] | undefined;\n}) {\n  let message = 'Results received: %o';\n  const values = [serializedResult];\n\n  if (serializedComponents !== undefined) {\n    message += ' (components: %o)';\n    values.push(serializedComponents);\n  }\n\n  debug(message, ...values);\n}\n"
  },
  {
    "path": "packages/component-client/src/index.ts",
    "content": "export * from './component-client';\nexport * from './utilities';\n"
  },
  {
    "path": "packages/component-client/src/utilities.ts",
    "content": "import type {ComponentClient} from './component-client';\n\nexport function isComponentClientClass(value: any): value is typeof ComponentClient {\n  return typeof value?.isComponentClient === 'function';\n}\n\nexport function isComponentClientInstance(value: any): value is ComponentClient {\n  return typeof value?.constructor?.isComponentClient === 'function';\n}\n"
  },
  {
    "path": "packages/component-client/tsconfig.json",
    "content": "{\n  \"extends\": \"@mvila/tsconfig\",\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/component-express-middleware/README.md",
    "content": "# @layr/component-koa-middleware\n\nA Koa middleware for your Layr components.\n\n## Installation\n\n```\nnpm install @layr/component-koa-middleware\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/component-express-middleware/package.json",
    "content": "{\n  \"name\": \"@layr/component-express-middleware\",\n  \"version\": \"2.0.94\",\n  \"description\": \"An Express middleware for your Layr components\",\n  \"keywords\": [\n    \"layr\",\n    \"component\",\n    \"express\",\n    \"middleware\"\n  ],\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/layrjs/layr/tree/master/packages/component-express-middleware\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"dist/node-cjs/index.js\",\n  \"module\": \"dist/node-esm/index.js\",\n  \"engines\": {\n    \"node\": \">=16.0.0\"\n  },\n  \"scripts\": {\n    \"build\": \"dev-tools build:ts-library\",\n    \"prepare\": \"npm run build\",\n    \"publish:package\": \"dev-tools publish:package\",\n    \"update\": \"dev-tools update:dependencies\"\n  },\n  \"dependencies\": {\n    \"@layr/component\": \"^2.0.51\",\n    \"@layr/component-server\": \"^2.0.70\",\n    \"@layr/routable\": \"^2.0.113\",\n    \"core-helpers\": \"^1.0.8\",\n    \"http-errors\": \"^1.8.1\",\n    \"mime-types\": \"^2.1.35\",\n    \"raw-body\": \"^2.5.1\",\n    \"sleep-promise\": \"^9.1.0\",\n    \"tslib\": \"^2.4.1\"\n  },\n  \"devDependencies\": {\n    \"@mvila/dev-tools\": \"^1.3.1\",\n    \"@mvila/tsconfig\": \"^1.0.6\",\n    \"@types/express\": \"^4.17.15\",\n    \"@types/http-errors\": \"^1.8.2\",\n    \"@types/mime-types\": \"^2.1.1\"\n  }\n}\n"
  },
  {
    "path": "packages/component-express-middleware/src/component-express-middleware.ts",
    "content": "/**\n * @module component-express-middleware\n *\n * An [Express](https://expressjs.com/) middleware allowing to serve a root [`Component`](https://layrjs.com/docs/v2/reference/component) so it can be accessed by a [`ComponentHTTPClient`](https://layrjs.com/docs/v2/reference/component-http-client).\n *\n * #### Usage\n *\n * Call the [`serveComponent()`](https://layrjs.com/docs/v2/reference/component-express-middleware#serve-component-function) function to create a middleware for your Express app.\n *\n * **Example:**\n *\n * ```\n * import express from 'express';\n * import {Component} from '@layr/component';\n * import {serveComponent} from '@layr/component-express-middleware';\n *\n * class Movie extends Component {\n *   // ...\n * }\n *\n * const app = express();\n *\n * app.use('/api', serveComponent(Movie));\n *\n * app.listen(3210);\n * ```\n */\n\nimport type {Component} from '@layr/component';\nimport {ensureComponentServer} from '@layr/component-server';\nimport type {ComponentServer, ComponentServerOptions} from '@layr/component-server';\nimport {callRouteByURL, isRoutableClass} from '@layr/routable';\nimport type {Request, Response} from 'express';\nimport getRawBody from 'raw-body';\nimport mime from 'mime-types';\nimport httpError from 'http-errors';\nimport sleep from 'sleep-promise';\n\nconst DEFAULT_LIMIT = '8mb';\n\nexport type ServeComponentOptions = ComponentServerOptions & {\n  limit?: number | string;\n  delay?: number;\n  errorRate?: number;\n};\n\n/**\n * Creates an [Express](https://expressjs.com/) middleware exposing the specified root [`Component`](https://layrjs.com/docs/v2/reference/component) class.\n *\n * @param componentOrComponentServer The root [`Component`](https://layrjs.com/docs/v2/reference/component) class to serve. An instance of a [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server) will be created under the hood. Alternatively, you can pass an existing instance of a [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server).\n * @param [options.version] A number specifying the version of the created [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server) (default: `undefined`).\n *\n * @returns An Express middleware.\n *\n * @category Functions\n */\nexport function serveComponent(\n  componentOrComponentServer: typeof Component | ComponentServer,\n  options: ServeComponentOptions = {}\n) {\n  const componentServer = ensureComponentServer(componentOrComponentServer, options);\n  const component = componentServer.getComponent();\n  const routableComponent = isRoutableClass(component) ? component : undefined;\n\n  const {limit = DEFAULT_LIMIT, delay = 0, errorRate = 0} = options;\n\n  return async function (req: Request, res: Response) {\n    if (delay > 0) {\n      await sleep(delay);\n    }\n\n    if (errorRate > 0) {\n      const threshold = errorRate / 100;\n\n      if (Math.random() < threshold) {\n        throw httpError(500, 'A simulated error occurred while handling a component server query');\n      }\n    }\n\n    const method = req.method;\n    const url = req.url;\n    const headers = (req.headers as Record<string, string>) ?? {};\n    const contentType = headers['content-type'] || 'application/octet-stream';\n    const charset = mime.charset(contentType) || undefined;\n    const body: string | Buffer = await getRawBody(req, {limit, encoding: charset});\n\n    if (url === '/') {\n      if (method === 'GET') {\n        res.json(await componentServer.receive({query: {'introspect=>': {'()': []}}}));\n        return;\n      }\n\n      if (method === 'POST') {\n        let parsedBody: any;\n\n        if (typeof body !== 'string') {\n          throw new Error(\n            `Expected a body of type 'string', but received a value of type '${typeof body}'`\n          );\n        }\n\n        try {\n          parsedBody = JSON.parse(body);\n        } catch (error) {\n          throw new Error(`An error occurred while parsing a JSON body string ('${body}')`);\n        }\n\n        const {query, components, version} = parsedBody;\n        res.json(await componentServer.receive({query, components, version}));\n        return;\n      }\n\n      throw httpError(405);\n    }\n\n    if (routableComponent !== undefined) {\n      const routableComponentFork = routableComponent.fork();\n\n      await routableComponentFork.initialize();\n\n      const routeResponse: {\n        status: number;\n        headers?: Record<string, string>;\n        body?: string | Buffer;\n      } = await callRouteByURL(routableComponentFork, url, {method, headers, body});\n\n      if (typeof routeResponse?.status !== 'number') {\n        throw new Error(\n          `Unexpected response \\`${JSON.stringify(\n            routeResponse\n          )}\\` returned by a component route (a proper response should be an object of the shape \\`{status: number; headers?: Record<string, string>; body?: string | Buffer;}\\`)`\n        );\n      }\n\n      res.status(routeResponse.status);\n\n      if (routeResponse.headers !== undefined) {\n        res.set(routeResponse.headers);\n      }\n\n      if (routeResponse.body !== undefined) {\n        res.send(routeResponse.body);\n      } else {\n        res.end();\n      }\n\n      return;\n    }\n\n    throw httpError(404);\n  };\n}\n"
  },
  {
    "path": "packages/component-express-middleware/src/index.ts",
    "content": "export * from './component-express-middleware';\n"
  },
  {
    "path": "packages/component-express-middleware/tsconfig.json",
    "content": "{\n  \"extends\": \"@mvila/tsconfig\",\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/component-http-client/.gitignore",
    "content": ".DS_STORE\nnode_modules\n*.log\n/dist\n"
  },
  {
    "path": "packages/component-http-client/README.md",
    "content": "# @layr/component-http-client\n\nAn HTTP client for your Layr components.\n\n## Installation\n\n```\nnpm install @layr/component-http-client\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/component-http-client/package.json",
    "content": "{\n  \"name\": \"@layr/component-http-client\",\n  \"version\": \"2.0.76\",\n  \"description\": \"An HTTP client for your Layr components\",\n  \"keywords\": [\n    \"layr\",\n    \"component\",\n    \"http\",\n    \"client\"\n  ],\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/layrjs/layr/tree/master/packages/component-http-client\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"dist/node-cjs/index.js\",\n  \"module\": \"dist/node-esm/index.js\",\n  \"engines\": {\n    \"node\": \">=16.0.0\"\n  },\n  \"scripts\": {\n    \"build\": \"dev-tools build:ts-library\",\n    \"prepare\": \"npm run build\",\n    \"test\": \"dev-tools test:ts-library\",\n    \"publish:package\": \"dev-tools publish:package\",\n    \"update\": \"dev-tools update:dependencies\"\n  },\n  \"dependencies\": {\n    \"@layr/component-client\": \"^2.0.86\",\n    \"@layr/utilities\": \"^1.0.9\",\n    \"core-helpers\": \"^1.0.8\",\n    \"cross-fetch\": \"^3.1.5\",\n    \"tslib\": \"^2.4.1\"\n  },\n  \"devDependencies\": {\n    \"@koa/cors\": \"^3.4.3\",\n    \"@layr/component\": \"^2.0.51\",\n    \"@mvila/dev-tools\": \"^1.3.1\",\n    \"@mvila/tsconfig\": \"^1.0.6\",\n    \"@types/co-body\": \"^5.1.1\",\n    \"@types/jest\": \"^29.2.5\",\n    \"@types/koa\": \"^2.13.5\",\n    \"@types/koa__cors\": \"^3.3.0\",\n    \"@types/koa-json-error\": \"^3.1.4\",\n    \"@types/lodash\": \"^4.14.191\",\n    \"co-body\": \"^6.1.0\",\n    \"koa\": \"^2.14.1\",\n    \"koa-json-error\": \"^3.1.2\",\n    \"lodash\": \"^4.17.21\"\n  }\n}\n"
  },
  {
    "path": "packages/component-http-client/src/component-http-client.test.ts",
    "content": "import {isComponentClass} from '@layr/component';\nimport Koa from 'koa';\nimport jsonError from 'koa-json-error';\nimport cors from '@koa/cors';\nimport body from 'co-body';\nimport type {Server} from 'http';\nimport isEqual from 'lodash/isEqual';\n\nimport {ComponentHTTPClient} from './component-http-client';\n\nconst SERVER_PORT = Math.floor(Math.random() * (60000 - 50000 + 1) + 50000);\n\ndescribe('ComponentHTTPClient', () => {\n  let server: Server | undefined;\n\n  beforeAll(() => {\n    const koa = new Koa();\n\n    koa.use(jsonError());\n\n    koa.use(cors({maxAge: 900})); // 15 minutes\n\n    koa.use(async function (ctx) {\n      const {query, version: clientVersion} = await body.json(ctx.req);\n\n      const serverVersion = 1;\n\n      if (clientVersion !== serverVersion) {\n        throw Object.assign(\n          new Error(\n            `The component client version (${clientVersion}) doesn't match the component server version (${serverVersion})`\n          ),\n          {code: 'COMPONENT_CLIENT_VERSION_DOES_NOT_MATCH_COMPONENT_SERVER_VERSION', expose: true}\n        );\n      }\n\n      if (isEqual(query, {'introspect=>': {'()': []}})) {\n        ctx.body = {\n          result: {\n            component: {\n              name: 'Movie',\n              properties: [\n                {\n                  name: 'limit',\n                  type: 'Attribute',\n                  valueType: 'number',\n                  value: 100,\n                  exposure: {get: true}\n                }\n              ]\n            }\n          }\n        };\n      } else {\n        throw new Error(`Received an unknown query: ${JSON.stringify(query)}`);\n      }\n    });\n\n    return new Promise<void>((resolve) => {\n      server = koa.listen(SERVER_PORT, resolve);\n    });\n  });\n\n  afterAll(() => {\n    if (server === undefined) {\n      return;\n    }\n\n    return new Promise<void>((resolve) => {\n      server!.close(() => {\n        server = undefined;\n        resolve();\n      });\n    });\n  });\n\n  test('Getting components', async () => {\n    let client = new ComponentHTTPClient(`http://localhost:${SERVER_PORT}`);\n\n    await expect(client.getComponent()).rejects.toThrow(\n      \"The component client version (undefined) doesn't match the component server version (1)\"\n    );\n\n    client = new ComponentHTTPClient(`http://localhost:${SERVER_PORT}`, {version: 1});\n\n    const Movie = await client.getComponent();\n\n    expect(isComponentClass(Movie)).toBe(true);\n    expect(Movie.getComponentName()).toBe('Movie');\n\n    const attribute = Movie.getAttribute('limit');\n\n    expect(attribute.getValue()).toBe(100);\n    expect(attribute.getExposure()).toEqual({get: true});\n  });\n});\n"
  },
  {
    "path": "packages/component-http-client/src/component-http-client.ts",
    "content": "import {ComponentClient, ComponentClientOptions} from '@layr/component-client';\nimport fetch from 'cross-fetch';\nimport {sleep} from '@layr/utilities';\nimport type {PlainObject} from 'core-helpers';\n\nconst DEFAULT_MAXIMUM_REQUEST_RETRIES = 10;\nconst DEFAULT_MINIMUM_TIME_BETWEEN_REQUEST_RETRIES = 3000; // 3 seconds\n\nexport type ComponentHTTPClientOptions = ComponentClientOptions & {\n  retryFailedRequests?: RetryFailedRequests;\n  maximumRequestRetries?: number;\n  minimumTimeBetweenRequestRetries?: number;\n};\n\nexport type RetryFailedRequests =\n  | boolean\n  | (({error, numberOfRetries}: {error: Error; numberOfRetries: number}) => Promise<boolean>);\n\n/**\n * *Inherits from [`ComponentClient`](https://layrjs.com/docs/v2/reference/component-client).*\n *\n * A class allowing to access a root [`Component`](https://layrjs.com/docs/v2/reference/component) that is served by a [`ComponentHTTPServer`](https://layrjs.com/docs/v2/reference/component-http-server), a middleware such as [`component-express-middleware`](https://layrjs.com/docs/v2/reference/component-express-middleware), or any HTTP server exposing a [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server).\n *\n * #### Usage\n *\n * Create an instance of `ComponentHTTPClient` by specifying the URL of the component server, and use the [`getComponent()`](https://layrjs.com/docs/v2/reference/component-http-client#get-component-instance-method) method to get the served component.\n *\n * For example, to access a `Movie` component that is served by a component server, you could do the following:\n *\n * ```\n * // JS\n *\n * // backend.js\n *\n * import {Component, attribute, method, expose} from '@layr/component';\n * import {ComponentHTTPServer} from '@layr/component-http-server';\n *\n * export class Movie extends Component {\n *   @expose({get: true, set: true}) @attribute('string') title;\n *\n *   @expose({call: true}) @method() async play() {\n *     return `Playing `${this.title}`...`;\n *   }\n * }\n *\n * const server = new ComponentHTTPServer(Movie, {port: 3210});\n *\n * server.start();\n * ```\n *\n * ```\n * // JS\n *\n * // frontend.js\n *\n * import {ComponentHTTPClient} from '@layr/component-http-client';\n *\n * (async () => {\n *   const client = new ComponentHTTPClient('http://localhost:3210');\n *\n *   const Movie = await client.getComponent();\n *\n *   const movie = new Movie({title: 'Inception'});\n *\n *   await movie.play(); // => 'Playing Inception...'\n * })();\n * ```\n *\n * ```\n * // TS\n *\n * // backend.ts\n *\n * import {Component, attribute, method, expose} from '@layr/component';\n * import {ComponentHTTPServer} from '@layr/component-http-server';\n *\n * export class Movie extends Component {\n *   @expose({get: true, set: true}) @attribute('string') title!: string;\n *\n *   @expose({call: true}) @method() async play() {\n *     return `Playing `${this.title}`...`;\n *   }\n * }\n *\n * const server = new ComponentHTTPServer(Movie, {port: 3210});\n *\n * server.start();\n * ```\n *\n * ```\n * // TS\n *\n * // frontend.ts\n *\n * import {ComponentHTTPClient} from '@layr/component-http-client';\n *\n * import type {Movie as MovieType} from './backend';\n *\n * (async () => {\n *   const client = new ComponentHTTPClient('http://localhost:3210');\n *\n *   const Movie = (await client.getComponent()) as typeof MovieType;\n *\n *   const movie = new Movie({title: 'Inception'});\n *\n *   await movie.play(); // => 'Playing Inception...'\n * })();\n * ```\n */\nexport class ComponentHTTPClient extends ComponentClient {\n  /**\n   * Creates a component HTTP client.\n   *\n   * @param url A string specifying the URL of the component server to connect to.\n   * @param [options.version] A number specifying the expected version of the component server (default: `undefined`). If a version is specified, an error is thrown when a request is sent and the component server has a different version. The thrown error is a JavaScript `Error` instance with a `code` attribute set to `'COMPONENT_CLIENT_VERSION_DOES_NOT_MATCH_COMPONENT_SERVER_VERSION'`.\n   * @param [options.mixins] An array of the component mixins (e.g., [`Storable`](https://layrjs.com/docs/v2/reference/storable)) to use when constructing the components exposed by the component server (default: `[]`).\n   * @param [options.retryFailedRequests] A boolean or a function returning a boolean specifying whether a request should be retried in case of a network issue (default: `false`). In case a function is specified, the function will receive an object of the shape `{error, numberOfRetries}` where `error` is the error that has occurred and `numberOfRetries` is the number of retries that has been attempted so far. The function can be asynchronous ans should return a boolean.\n   * @param [options.maximumRequestRetries] The maximum number of times a request can be retried (default: `10`).\n   * @param [options.minimumTimeBetweenRequestRetries] A number specifying the minimum time in milliseconds that should elapse between each request retry (default: `3000`).\n   *\n   * @returns A `ComponentHTTPClient` instance.\n   *\n   * @category Creation\n   */\n  constructor(url: string, options: ComponentHTTPClientOptions = {}) {\n    const {\n      retryFailedRequests = false,\n      maximumRequestRetries = DEFAULT_MAXIMUM_REQUEST_RETRIES,\n      minimumTimeBetweenRequestRetries = DEFAULT_MINIMUM_TIME_BETWEEN_REQUEST_RETRIES,\n      ...componentClientOptions\n    } = options;\n\n    const componentServer = createComponentServer(url, {\n      retryFailedRequests,\n      maximumRequestRetries,\n      minimumTimeBetweenRequestRetries\n    });\n\n    super(componentServer, {...componentClientOptions, batchable: true});\n  }\n\n  /**\n   * @method getComponent\n   *\n   * Gets the component that is served by the component server.\n   *\n   * @returns A [`Component`](https://layrjs.com/docs/v2/reference/component) class.\n   *\n   * @examplelink See an [example of use](https://layrjs.com/docs/v2/reference/component-http-client#usage) above.\n   *\n   * @category Getting the Served Component\n   * @async\n   */\n}\n\nfunction createComponentServer(\n  url: string,\n  {\n    retryFailedRequests,\n    maximumRequestRetries,\n    minimumTimeBetweenRequestRetries\n  }: {\n    retryFailedRequests: RetryFailedRequests;\n    maximumRequestRetries: number;\n    minimumTimeBetweenRequestRetries: number;\n  }\n) {\n  return {\n    async receive(request: {query: PlainObject; components?: PlainObject[]; version?: number}) {\n      const {query, components, version} = request;\n\n      const jsonRequest = {query, components, version};\n\n      let numberOfRetries = 0;\n\n      while (true) {\n        let fetchResponse: Response;\n        let jsonResponse: any;\n\n        try {\n          fetchResponse = await fetch(url, {\n            method: 'POST',\n            headers: {'Content-Type': 'application/json'},\n            body: JSON.stringify(jsonRequest)\n          });\n\n          jsonResponse = await fetchResponse.json();\n        } catch (error: any) {\n          if (numberOfRetries < maximumRequestRetries) {\n            const startTime = Date.now();\n\n            const shouldRetry =\n              typeof retryFailedRequests === 'function'\n                ? await retryFailedRequests({error, numberOfRetries})\n                : retryFailedRequests;\n\n            if (shouldRetry) {\n              const elapsedTime = Date.now() - startTime;\n\n              if (elapsedTime < minimumTimeBetweenRequestRetries) {\n                await sleep(minimumTimeBetweenRequestRetries - elapsedTime);\n              }\n\n              numberOfRetries++;\n              continue;\n            }\n          }\n\n          throw error;\n        }\n\n        if (fetchResponse.status !== 200) {\n          const {\n            message = 'An error occurred while sending query to remote components',\n            ...attributes\n          } = jsonResponse ?? {};\n\n          throw Object.assign(new Error(message), attributes);\n        }\n\n        return jsonResponse;\n      }\n    }\n  };\n}\n"
  },
  {
    "path": "packages/component-http-client/src/index.ts",
    "content": "export * from './component-http-client';\n"
  },
  {
    "path": "packages/component-http-client/tsconfig.json",
    "content": "{\n  \"extends\": \"@mvila/tsconfig\",\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/component-http-server/README.md",
    "content": "# @layr/component-http-server\n\nA basic HTTP server for your Layr components.\n\n## Installation\n\n```\nnpm install @layr/component-http-server\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/component-http-server/package.json",
    "content": "{\n  \"name\": \"@layr/component-http-server\",\n  \"version\": \"2.0.97\",\n  \"description\": \"A basic HTTP server for your Layr components\",\n  \"keywords\": [\n    \"layr\",\n    \"component\",\n    \"http\",\n    \"server\"\n  ],\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/layrjs/layr/tree/master/packages/component-http-server\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"dist/node-cjs/index.js\",\n  \"module\": \"dist/node-esm/index.js\",\n  \"engines\": {\n    \"node\": \">=16.0.0\"\n  },\n  \"scripts\": {\n    \"build\": \"dev-tools build:ts-library\",\n    \"prepare\": \"npm run build\",\n    \"test\": \"dev-tools test:ts-library\",\n    \"publish:package\": \"dev-tools publish:package\",\n    \"update\": \"dev-tools update:dependencies\"\n  },\n  \"dependencies\": {\n    \"@koa/cors\": \"^3.4.3\",\n    \"@layr/component-koa-middleware\": \"^2.0.94\",\n    \"@layr/component-server\": \"^2.0.70\",\n    \"core-helpers\": \"^1.0.8\",\n    \"debug\": \"^4.3.4\",\n    \"koa\": \"^2.14.1\",\n    \"koa-json-error\": \"^3.1.2\",\n    \"koa-logger\": \"^3.2.1\",\n    \"tslib\": \"^2.4.1\"\n  },\n  \"devDependencies\": {\n    \"@layr/component\": \"^2.0.51\",\n    \"@mvila/dev-tools\": \"^1.3.1\",\n    \"@mvila/tsconfig\": \"^1.0.6\",\n    \"@types/debug\": \"^4.1.7\",\n    \"@types/jest\": \"^29.2.5\",\n    \"@types/koa\": \"^2.13.5\",\n    \"@types/koa__cors\": \"^3.3.0\",\n    \"@types/koa-json-error\": \"^3.1.4\",\n    \"@types/koa-logger\": \"^3.1.2\",\n    \"cross-fetch\": \"^3.1.5\"\n  }\n}\n"
  },
  {
    "path": "packages/component-http-server/src/component-http-server.test.ts",
    "content": "import {Component, attribute, expose} from '@layr/component';\nimport fetch from 'cross-fetch';\n\nimport {ComponentHTTPServer} from './component-http-server';\n\nconst SERVER_PORT = Math.floor(Math.random() * (60000 - 50000 + 1) + 50000);\n\ndescribe('ComponentHTTPServer', () => {\n  let server: ComponentHTTPServer;\n\n  beforeAll(async () => {\n    class Movie extends Component {\n      @expose({get: true}) @attribute('number') static limit = 100;\n    }\n\n    server = new ComponentHTTPServer(Movie, {port: SERVER_PORT});\n\n    await server.start();\n  });\n\n  afterAll(async () => {\n    await server?.stop();\n  });\n\n  test('Introspecting components', async () => {\n    const expectedResponse = {\n      result: {\n        component: {\n          name: 'Movie',\n          properties: [\n            {\n              name: 'limit',\n              type: 'Attribute',\n              valueType: 'number',\n              value: 100,\n              exposure: {get: true}\n            }\n          ]\n        }\n      }\n    };\n\n    expect(await get()).toStrictEqual(expectedResponse);\n\n    expect(await postJSON({query: {'introspect=>': {'()': []}}})).toStrictEqual(expectedResponse);\n\n    await expect(postJSON({query: {'introspect=>': {'()': []}}, version: 1})).rejects.toThrow(\n      \"The component client version (1) doesn't match the component server version (undefined)\"\n    );\n  });\n});\n\nasync function get() {\n  const url = `http://localhost:${SERVER_PORT}`;\n\n  const response = await fetch(url);\n\n  return await handleFetchResponse(response);\n}\n\nasync function postJSON(json: object) {\n  const url = `http://localhost:${SERVER_PORT}`;\n\n  const response = await fetch(url, {\n    method: 'POST',\n    headers: {'Content-Type': 'application/json'},\n    body: JSON.stringify(json)\n  });\n\n  return await handleFetchResponse(response);\n}\n\nasync function handleFetchResponse(response: Response) {\n  const result = await response.json();\n\n  if (response.status !== 200) {\n    const {message = 'An error occurred while sending query to remote components', ...attributes} =\n      result ?? {};\n\n    throw Object.assign(new Error(message), attributes);\n  }\n\n  return result;\n}\n"
  },
  {
    "path": "packages/component-http-server/src/component-http-server.ts",
    "content": "import Koa from 'koa';\nimport type {Server} from 'http';\nimport logger from 'koa-logger';\nimport jsonError from 'koa-json-error';\nimport cors from '@koa/cors';\nimport type {Component} from '@layr/component';\nimport type {ComponentServer} from '@layr/component-server';\nimport {ensureComponentServer} from '@layr/component-server';\nimport {serveComponent, ServeComponentOptions} from '@layr/component-koa-middleware';\nimport debugModule from 'debug';\n\nconst debug = debugModule('layr:component-http-server');\n// To display the debug log, set this environment:\n// DEBUG=layr:component-http-server DEBUG_DEPTH=10\n\nconst DEFAULT_PORT = 3333;\n\nexport type ComponentHTTPServerOptions = {port?: number} & ServeComponentOptions;\n\n/**\n * A class allowing to serve a root [`Component`](https://layrjs.com/docs/v2/reference/component) so it can be accessed by a [`ComponentHTTPClient`](https://layrjs.com/docs/v2/reference/component-http-client).\n *\n * This class provides a basic HTTP server providing one endpoint to serve your root component. If you wish to build an HTTP server providing multiple endpoints, you can use a middleware such as [`component-express-middleware`](https://layrjs.com/docs/v2/reference/component-express-middleware), or implement the necessary plumbing to integrate a [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server) in your custom HTTP server.\n *\n * #### Usage\n *\n * Create an instance of `ComponentHTTPServer` by specifying the root [`Component`](https://layrjs.com/docs/v2/reference/component) you want to serve, and use the [`start()`](https://layrjs.com/docs/v2/reference/component-http-server#start-instance-method) method to start the server.\n *\n * See an example of use in [`ComponentHTTPClient`](https://layrjs.com/docs/v2/reference/component-http-client).\n */\nexport class ComponentHTTPServer {\n  _componentServer: ComponentServer;\n  _port: number;\n  _serveComponentOptions: ServeComponentOptions;\n\n  /**\n   * Creates a component HTTP server.\n   *\n   * @param componentOrComponentServer The root [`Component`](https://layrjs.com/docs/v2/reference/component) class to serve. An instance of a [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server) will be created under the hood. Alternatively, you can pass an existing instance of a [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server).\n   * @param [options.port] A number specifying the TCP port to listen to (default: `3333`).\n   * @param [options.version] A number specifying the version of the created [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server) (default: `undefined`).\n   *\n   * @returns A `ComponentHTTPServer` instance.\n   *\n   * @category Creation\n   */\n  constructor(\n    componentOrComponentServer: typeof Component | ComponentServer,\n    options: ComponentHTTPServerOptions = {}\n  ) {\n    const componentServer = ensureComponentServer(componentOrComponentServer, options);\n\n    const {port = DEFAULT_PORT, limit, delay, errorRate} = options;\n\n    this._componentServer = componentServer;\n    this._port = port;\n    this._serveComponentOptions = {limit, delay, errorRate};\n  }\n\n  _server: Server | undefined;\n\n  /**\n   * Starts the component HTTP server.\n   *\n   * @example\n   * ```\n   * const server = new ComponentHTTPServer(Movie, {port: 3210});\n   *\n   * await server.start();\n   * ```\n   *\n   * @category Methods\n   * @async\n   */\n  start() {\n    if (this._server !== undefined) {\n      throw new Error('The component HTTP server is already started');\n    }\n\n    const koa = new Koa();\n\n    koa.use(\n      logger((message) => {\n        debug(message);\n      })\n    );\n\n    koa.use(jsonError());\n\n    koa.use(cors({maxAge: 900})); // 15 minutes\n\n    koa.use(serveComponent(this._componentServer, this._serveComponentOptions));\n\n    return new Promise<void>((resolve) => {\n      this._server = koa.listen(this._port, () => {\n        debug(`Component HTTP server started (port: ${this._port})`);\n        resolve();\n      });\n    });\n  }\n\n  /**\n   * Stops the component HTTP server.\n   *\n   * @category Methods\n   * @async\n   */\n  stop() {\n    const server = this._server;\n\n    if (server === undefined) {\n      throw new Error('The component HTTP server is not started');\n    }\n\n    return new Promise<void>((resolve) => {\n      server.close(() => {\n        this._server = undefined;\n        debug(`Component HTTP server stopped`);\n        resolve();\n      });\n    });\n  }\n}\n"
  },
  {
    "path": "packages/component-http-server/src/index.ts",
    "content": "export * from './component-http-server';\n"
  },
  {
    "path": "packages/component-http-server/tsconfig.json",
    "content": "{\n  \"extends\": \"@mvila/tsconfig\",\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/component-koa-middleware/README.md",
    "content": "# @layr/component-koa-middleware\n\nA Koa middleware for your Layr components.\n\n## Installation\n\n```\nnpm install @layr/component-koa-middleware\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/component-koa-middleware/package.json",
    "content": "{\n  \"name\": \"@layr/component-koa-middleware\",\n  \"version\": \"2.0.94\",\n  \"description\": \"A Koa middleware for your Layr components\",\n  \"keywords\": [\n    \"layr\",\n    \"component\",\n    \"koa\",\n    \"middleware\"\n  ],\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/layrjs/layr/tree/master/packages/component-koa-middleware\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"dist/node-cjs/index.js\",\n  \"module\": \"dist/node-esm/index.js\",\n  \"engines\": {\n    \"node\": \">=16.0.0\"\n  },\n  \"scripts\": {\n    \"build\": \"dev-tools build:ts-library\",\n    \"prepare\": \"npm run build\",\n    \"publish:package\": \"dev-tools publish:package\",\n    \"update\": \"dev-tools update:dependencies\"\n  },\n  \"dependencies\": {\n    \"@layr/component\": \"^2.0.51\",\n    \"@layr/component-server\": \"^2.0.70\",\n    \"@layr/routable\": \"^2.0.113\",\n    \"core-helpers\": \"^1.0.8\",\n    \"mime-types\": \"^2.1.35\",\n    \"raw-body\": \"^2.5.1\",\n    \"sleep-promise\": \"^9.1.0\",\n    \"tslib\": \"^2.4.1\"\n  },\n  \"devDependencies\": {\n    \"@mvila/dev-tools\": \"^1.3.1\",\n    \"@mvila/tsconfig\": \"^1.0.6\",\n    \"@types/koa\": \"^2.13.5\",\n    \"@types/mime-types\": \"^2.1.1\"\n  }\n}\n"
  },
  {
    "path": "packages/component-koa-middleware/src/component-koa-middleware.ts",
    "content": "/**\n * @module component-koa-middleware\n *\n * A [Koa](https://koajs.com/) middleware allowing to serve a root [`Component`](https://layrjs.com/docs/v2/reference/component) so it can be accessed by a [`ComponentHTTPClient`](https://layrjs.com/docs/v2/reference/component-http-client).\n *\n * #### Usage\n *\n * Call the [`serveComponent()`](https://layrjs.com/docs/v2/reference/component-koa-middleware#serve-component-function) function to create a middleware for your Koa app.\n *\n * **Example:**\n *\n * ```\n * import Koa from 'koa';\n * import {Component} from '@layr/component';\n * import {serveComponent} from '@layr/component-koa-middleware';\n *\n * class Movie extends Component {\n *   // ...\n * }\n *\n * const app = new Koa();\n *\n * // Serve the `Movie` component at the root ('/')\n * app.use(serveComponent(Movie));\n *\n * app.listen(3210);\n * ```\n *\n * If you want to serve your component at a specific URL, you can use [`koa-mount`](https://github.com/koajs/mount):\n *\n * ```\n * import mount from 'koa-mount';\n *\n * // Serve the `Movie` component at a specific URL ('/api')\n * app.use(mount('/api', serveComponent(Movie)));\n * ```\n */\n\nimport type {Component} from '@layr/component';\nimport {ensureComponentServer} from '@layr/component-server';\nimport type {ComponentServer, ComponentServerOptions} from '@layr/component-server';\nimport {callRouteByURL, isRoutableClass} from '@layr/routable';\nimport type {Context} from 'koa';\nimport getRawBody from 'raw-body';\nimport mime from 'mime-types';\nimport sleep from 'sleep-promise';\n\nconst DEFAULT_LIMIT = '8mb';\n\nexport type ServeComponentOptions = ComponentServerOptions & {\n  limit?: number | string;\n  delay?: number;\n  errorRate?: number;\n};\n\n/**\n * Creates a [Koa](https://koajs.com/) middleware exposing the specified root [`Component`](https://layrjs.com/docs/v2/reference/component) class.\n *\n * @param componentOrComponentServer The root [`Component`](https://layrjs.com/docs/v2/reference/component) class to serve. An instance of a [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server) will be created under the hood. Alternatively, you can pass an existing instance of a [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server).\n * @param [options.version] A number specifying the version of the created [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server) (default: `undefined`).\n *\n * @returns A Koa middleware.\n *\n * @category Functions\n */\nexport function serveComponent(\n  componentOrComponentServer: typeof Component | ComponentServer,\n  options: ServeComponentOptions = {}\n) {\n  const componentServer = ensureComponentServer(componentOrComponentServer, options);\n  const component = componentServer.getComponent();\n  const routableComponent = isRoutableClass(component) ? component : undefined;\n\n  const {limit = DEFAULT_LIMIT, delay = 0, errorRate = 0} = options;\n\n  return async function (ctx: Context) {\n    if (delay > 0) {\n      await sleep(delay);\n    }\n\n    if (errorRate > 0) {\n      const threshold = errorRate / 100;\n\n      if (Math.random() < threshold) {\n        throw new Error('A simulated error occurred while handling a component server query');\n      }\n    }\n\n    const method = ctx.request.method;\n    const url = ctx.request.url;\n    const headers = (ctx.request.headers as Record<string, string>) ?? {};\n    const contentType = headers['content-type'] || 'application/octet-stream';\n    const charset = mime.charset(contentType) || undefined;\n    const body: string | Buffer = await getRawBody(ctx.req, {limit, encoding: charset});\n\n    if (url === '/') {\n      if (method === 'GET') {\n        ctx.response.body = await componentServer.receive({query: {'introspect=>': {'()': []}}});\n        return;\n      }\n\n      if (method === 'POST') {\n        let parsedBody: any;\n\n        if (typeof body !== 'string') {\n          throw new Error(\n            `Expected a body of type 'string', but received a value of type '${typeof body}'`\n          );\n        }\n\n        try {\n          parsedBody = JSON.parse(body);\n        } catch (error) {\n          throw new Error(`An error occurred while parsing a JSON body string ('${body}')`);\n        }\n\n        const {query, components, version} = parsedBody;\n        ctx.response.body = await componentServer.receive({query, components, version});\n        return;\n      }\n\n      ctx.throw(405);\n    }\n\n    if (routableComponent !== undefined) {\n      const routableComponentFork = routableComponent.fork();\n\n      await routableComponentFork.initialize();\n\n      const routeResponse: {\n        status: number;\n        headers?: Record<string, string>;\n        body?: string | Buffer;\n      } = await callRouteByURL(routableComponentFork, url, {method, headers, body});\n\n      if (typeof routeResponse?.status !== 'number') {\n        throw new Error(\n          `Unexpected response \\`${JSON.stringify(\n            routeResponse\n          )}\\` returned by a component route (a proper response should be an object of the shape \\`{status: number; headers?: Record<string, string>; body?: string | Buffer;}\\`)`\n        );\n      }\n\n      ctx.response.status = routeResponse.status;\n\n      if (routeResponse.headers !== undefined) {\n        ctx.response.set(routeResponse.headers);\n      }\n\n      if (routeResponse.body !== undefined) {\n        ctx.response.body = routeResponse.body;\n      }\n\n      return;\n    }\n\n    ctx.throw(404);\n  };\n}\n"
  },
  {
    "path": "packages/component-koa-middleware/src/index.ts",
    "content": "export * from './component-koa-middleware';\n"
  },
  {
    "path": "packages/component-koa-middleware/tsconfig.json",
    "content": "{\n  \"extends\": \"@mvila/tsconfig\",\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/component-server/README.md",
    "content": "# @layr/component-server\n\nServes your Layr components.\n\n## Installation\n\n```\nnpm install @layr/component-server\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/component-server/package.json",
    "content": "{\n  \"name\": \"@layr/component-server\",\n  \"version\": \"2.0.70\",\n  \"description\": \"Serves your Layr components\",\n  \"keywords\": [\n    \"layr\",\n    \"component\",\n    \"server\"\n  ],\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/layrjs/layr/tree/master/packages/component-server\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"dist/node-cjs/index.js\",\n  \"module\": \"dist/node-esm/index.js\",\n  \"engines\": {\n    \"node\": \">=16.0.0\"\n  },\n  \"scripts\": {\n    \"build\": \"dev-tools build:ts-library\",\n    \"prepare\": \"npm run build\",\n    \"test\": \"dev-tools test:ts-library\",\n    \"publish:package\": \"dev-tools publish:package\",\n    \"update\": \"dev-tools update:dependencies\"\n  },\n  \"dependencies\": {\n    \"@deepr/runtime\": \"^1.1.0\",\n    \"@layr/component\": \"^2.0.51\",\n    \"core-helpers\": \"^1.0.8\",\n    \"debug\": \"^4.3.4\",\n    \"lodash\": \"^4.17.21\",\n    \"possibly-async\": \"^1.0.7\",\n    \"tslib\": \"^2.4.1\"\n  },\n  \"devDependencies\": {\n    \"@mvila/dev-tools\": \"^1.3.1\",\n    \"@mvila/tsconfig\": \"^1.0.6\",\n    \"@types/debug\": \"^4.1.7\",\n    \"@types/jest\": \"^29.2.5\",\n    \"@types/lodash\": \"^4.14.191\"\n  }\n}\n"
  },
  {
    "path": "packages/component-server/src/component-server.test.ts",
    "content": "import {\n  Component,\n  attribute,\n  primaryIdentifier,\n  secondaryIdentifier,\n  method,\n  expose,\n  provide,\n  consume,\n  validators\n} from '@layr/component';\nimport {PlainObject, forEachDeep} from 'core-helpers';\n\nimport {ComponentServer} from './component-server';\n\ndescribe('ComponentServer', () => {\n  test('Introspecting components', async () => {\n    class Session extends Component {\n      @expose({get: true, set: true}) @attribute('string?') static token?: string;\n    }\n\n    class Movie extends Component {\n      @consume() static Session: typeof Session;\n\n      @expose({call: true}) @method() static find() {}\n      @method() static count() {}\n\n      @expose({get: true, set: true}) @primaryIdentifier() id!: string;\n      @expose({get: true, set: true}) @secondaryIdentifier() slug!: string;\n      @expose({get: true, set: true})\n      @attribute('string', {validators: [validators.notEmpty()]})\n      title = '';\n      @expose({get: true}) @attribute('boolean') isPlaying = false;\n      @expose({call: true}) @method() play() {}\n      @method() delete() {}\n    }\n\n    class Backend extends Component {\n      @provide() static Session = Session;\n      @provide() static Movie = Movie;\n    }\n\n    const server = new ComponentServer(Backend);\n\n    const response = server.receive({query: {'introspect=>': {'()': []}}});\n    trimSerializedFunctions(response);\n\n    expect(response).toStrictEqual({\n      result: {\n        component: {\n          name: 'Backend',\n          providedComponents: [\n            {\n              name: 'Session',\n              properties: [\n                {\n                  name: 'token',\n                  type: 'Attribute',\n                  valueType: 'string?',\n                  value: {__undefined: true},\n                  exposure: {get: true, set: true}\n                }\n              ]\n            },\n            {\n              name: 'Movie',\n              properties: [{name: 'find', type: 'Method', exposure: {call: true}}],\n              prototype: {\n                properties: [\n                  {\n                    name: 'id',\n                    type: 'PrimaryIdentifierAttribute',\n                    valueType: 'string',\n                    default: {\n                      __function: 'function () {\\nreturn this.constructor.generateId();\\n}'\n                    },\n                    exposure: {get: true, set: true}\n                  },\n                  {\n                    name: 'slug',\n                    type: 'SecondaryIdentifierAttribute',\n                    valueType: 'string',\n                    exposure: {get: true, set: true}\n                  },\n                  {\n                    name: 'title',\n                    type: 'Attribute',\n                    valueType: 'string',\n                    default: {__function: \"function () {\\nreturn ''\\n}\"},\n                    validators: [\n                      {\n                        __validator: {\n                          name: 'notEmpty',\n                          function: {\n                            __function: '(value) => value !== undefined && value.length > 0'\n                          }\n                        }\n                      }\n                    ],\n                    exposure: {get: true, set: true}\n                  },\n                  {\n                    name: 'isPlaying',\n                    type: 'Attribute',\n                    valueType: 'boolean',\n                    exposure: {get: true}\n                  },\n                  {name: 'play', type: 'Method', exposure: {call: true}}\n                ]\n              },\n              consumedComponents: ['Session']\n            }\n          ]\n        }\n      }\n    });\n\n    expect(() => server.receive({query: {'introspect=>': {'()': []}}, version: 1})).toThrow(\n      \"The component client version (1) doesn't match the component server version (undefined)\"\n    );\n\n    function trimSerializedFunctions(object: PlainObject) {\n      forEachDeep(object, (value, name, node) => {\n        if (name === '__function') {\n          (node as any).__function = value.replace(/\\n +/g, '\\n');\n        }\n      });\n    }\n  });\n\n  test('Accessing attributes', async () => {\n    class Movie extends Component {\n      @attribute('number') static limit = 100;\n      @expose({get: true, set: true}) @attribute('number') static offset = 0;\n\n      @expose({get: true, set: true}) @primaryIdentifier('string') id!: string;\n      @expose({get: true, set: true}) @attribute('string') title = '';\n      @attribute('number?') rating?: number;\n    }\n\n    const server = new ComponentServer(Movie);\n\n    expect(\n      server.receive({\n        query: {'<=': {__component: 'typeof Movie'}}\n      })\n    ).toStrictEqual({\n      result: {__component: 'typeof Movie', offset: 0}\n    });\n\n    expect(\n      server.receive({\n        query: {\n          '<=': {__component: 'typeof Movie'},\n          'offset': true\n        }\n      })\n    ).toStrictEqual({\n      result: {offset: 0},\n      components: [{__component: 'typeof Movie', offset: 0}]\n    });\n\n    expect(\n      server.receive({\n        query: {\n          '<=': {__component: 'typeof Movie'},\n          'offset': true,\n          'limit': true\n        }\n      })\n    ).toStrictEqual({\n      result: {\n        offset: 0,\n        limit: {__error: \"Cannot get the value of an attribute that is not allowed (name: 'limit')\"}\n      },\n      components: [{__component: 'typeof Movie', offset: 0}]\n    });\n\n    expect(\n      server.receive({\n        query: {\n          '<=': {__component: 'Movie', __new: true, id: 'm1'}\n        }\n      })\n    ).toStrictEqual({\n      result: {__component: 'Movie', __new: true, id: 'm1', title: ''},\n      components: [{__component: 'typeof Movie', offset: 0}]\n    });\n\n    expect(\n      server.receive({\n        query: {\n          '<=': {__component: 'Movie', __new: true, id: 'm1'},\n          'title': true\n        },\n        components: [{__component: 'typeof Movie', offset: 0}]\n      })\n    ).toStrictEqual({\n      result: {title: ''},\n      components: [{__component: 'Movie', __new: true, id: 'm1', title: ''}]\n    });\n\n    expect(\n      server.receive({\n        query: {\n          '<=': {__component: 'Movie', __new: true, id: 'm1', title: 'Inception'}\n        }\n      })\n    ).toStrictEqual({\n      result: {__component: 'Movie', __new: true, id: 'm1'},\n      components: [{__component: 'typeof Movie', offset: 0}]\n    });\n\n    expect(\n      server.receive({\n        query: {\n          '<=': {__component: 'Movie', __new: true, id: 'm1', title: 'Inception'},\n          'title': true\n        }\n      })\n    ).toStrictEqual({\n      result: {title: 'Inception'},\n      components: [{__component: 'typeof Movie', offset: 0}]\n    });\n\n    expect(\n      server.receive({\n        query: {\n          '<=': {__component: 'Movie', id: 'm1'}\n        }\n      })\n    ).toStrictEqual({\n      result: {__component: 'Movie', id: 'm1'},\n      components: [{__component: 'typeof Movie', offset: 0}]\n    });\n\n    expect(\n      server.receive({\n        query: {\n          '<=': {__component: 'Movie', id: 'm1'},\n          'title': true\n        }\n      })\n    ).toStrictEqual({\n      result: {\n        title: {\n          __error: \"Cannot get the value of an unset attribute (attribute: 'Movie.prototype.title')\"\n        }\n      },\n      components: [{__component: 'typeof Movie', offset: 0}]\n    });\n\n    expect(\n      server.receive({\n        query: {\n          '<=': {__component: 'Movie', id: 'm1', rating: 10}\n        }\n      })\n    ).toStrictEqual({\n      result: {__component: 'Movie', id: 'm1'},\n      components: [{__component: 'typeof Movie', offset: 0}]\n    });\n\n    expect(\n      server.receive({\n        query: {\n          '<=': {__component: 'Movie', id: 'm1', rating: 10},\n          'rating': true\n        }\n      })\n    ).toStrictEqual({\n      result: {\n        rating: {\n          __error:\n            \"Cannot get the value of an unset attribute (attribute: 'Movie.prototype.rating')\"\n        }\n      },\n      components: [{__component: 'typeof Movie', offset: 0}]\n    });\n  });\n\n  test('Invoking methods', async () => {\n    class Movie extends Component {\n      @expose({call: true}) @method() static exposedClassMethod() {\n        return 'exposedClassMethod()';\n      }\n\n      @expose({call: true}) @method() static async exposedAsyncClassMethod() {\n        return 'exposedAsyncClassMethod()';\n      }\n\n      @method() static unexposedClassMethod() {\n        return 'unexposedClassMethod()';\n      }\n\n      @expose({call: true}) @method() exposedInstanceMethod() {\n        return 'exposedInstanceMethod()';\n      }\n\n      @expose({call: true}) @method() async exposedAsyncInstanceMethod() {\n        return 'exposedAsyncInstanceMethod()';\n      }\n\n      @method() unexposedInstanceMethod() {\n        return 'unexposedInstanceMethod()';\n      }\n\n      @expose({call: true}) @method() exposedInstanceMethodWithParameters(\n        param1: any,\n        param2: any\n      ) {\n        return `exposedInstanceMethodWithParameters(${param1}, ${param2})`;\n      }\n    }\n\n    const server = new ComponentServer(Movie);\n\n    expect(\n      server.receive({\n        query: {\n          '<=': {__component: 'typeof Movie'},\n          'exposedClassMethod=>': {\n            '()': []\n          }\n        }\n      })\n    ).toStrictEqual({result: 'exposedClassMethod()'});\n\n    expect(\n      await server.receive({\n        query: {\n          '<=': {__component: 'typeof Movie'},\n          'exposedAsyncClassMethod=>': {\n            '()': []\n          }\n        }\n      })\n    ).toStrictEqual({result: 'exposedAsyncClassMethod()'});\n\n    expect(\n      server.receive({\n        query: {\n          '<=': {__component: 'typeof Movie'},\n          'unexposedClassMethod=>': {\n            '()': []\n          }\n        }\n      })\n    ).toStrictEqual({\n      result: {\n        __error: \"Cannot execute a method that is not allowed (name: 'unexposedClassMethod')\"\n      }\n    });\n\n    expect(\n      server.receive({\n        query: {\n          '<=': {__component: 'Movie'},\n          'exposedInstanceMethod=>': {\n            '()': []\n          }\n        }\n      })\n    ).toStrictEqual({result: 'exposedInstanceMethod()'});\n\n    expect(\n      await server.receive({\n        query: {\n          '<=': {__component: 'Movie'},\n          'exposedAsyncInstanceMethod=>': {\n            '()': []\n          }\n        }\n      })\n    ).toStrictEqual({result: 'exposedAsyncInstanceMethod()'});\n\n    expect(\n      server.receive({\n        query: {\n          '<=': {__component: 'Movie'},\n          'unexposedInstanceMethod=>': {\n            '()': []\n          }\n        }\n      })\n    ).toStrictEqual({\n      result: {\n        __error: \"Cannot execute a method that is not allowed (name: 'unexposedInstanceMethod')\"\n      }\n    });\n\n    expect(\n      server.receive({\n        query: {\n          '<=': {__component: 'Movie'},\n          'exposedInstanceMethodWithParameters=>': {\n            '()': [1, 2]\n          }\n        }\n      })\n    ).toStrictEqual({result: 'exposedInstanceMethodWithParameters(1, 2)'});\n  });\n});\n"
  },
  {
    "path": "packages/component-server/src/component-server.ts",
    "content": "import {\n  Component,\n  ComponentSet,\n  IntrospectedComponent,\n  PropertyFilter,\n  Attribute,\n  PropertyOperation,\n  ExecutionMode,\n  isComponentClassOrInstance,\n  assertIsComponentClass,\n  serialize,\n  deserialize\n} from '@layr/component';\nimport {invokeQuery} from '@deepr/runtime';\nimport {possiblyAsync} from 'possibly-async';\nimport {PlainObject} from 'core-helpers';\nimport debugModule from 'debug';\n\n// To display the debug log, set this environment:\n// DEBUG=layr:component-server DEBUG_DEPTH=5\nconst debug = debugModule('layr:component-server');\n\n// To display errors occurring while invoking queries, set this environment:\n// DEBUG=layr:component-server:error DEBUG_DEPTH=5\nconst debugError = debugModule('layr:component-server:error');\n\nimport {isComponentServerInstance} from './utilities';\n\nexport interface ComponentServerLike {\n  receive: ComponentServer['receive'];\n}\n\nexport type ComponentServerOptions = {\n  name?: string;\n  version?: number;\n};\n\n/**\n * A base class allowing to serve a root [`Component`](https://layrjs.com/docs/v2/reference/component) so it can be accessed by a [`ComponentClient`](https://layrjs.com/docs/v2/reference/component-client).\n *\n * Typically, instead of using this class, you would use a class such as [`ComponentHTTPServer`](https://layrjs.com/docs/v2/reference/component-http-server), or a middleware such as [`component-express-middleware`](https://layrjs.com/docs/v2/reference/component-express-middleware).\n */\nexport class ComponentServer {\n  _component: typeof Component;\n  _introspectedComponent: IntrospectedComponent | undefined;\n  _name: string | undefined;\n  _version: number | undefined;\n\n  /**\n   * Creates a component server.\n   *\n   * @param component The root [`Component`](https://layrjs.com/docs/v2/reference/component) class to serve.\n   * @param [options.version] A number specifying the version of the returned component server (default: `undefined`).\n   *\n   * @returns A `ComponentServer` instance.\n   *\n   * @examplelink See [`ComponentClient`'s example](https://layrjs.com/docs/v2/reference/component-client#constructor).\n   *\n   * @category Creation\n   */\n  constructor(component: typeof Component, options: ComponentServerOptions = {}) {\n    const {name, version} = options;\n\n    assertIsComponentClass(component);\n\n    const introspectedComponent = component.introspect();\n\n    this._component = component;\n    this._introspectedComponent = introspectedComponent;\n    this._name = name;\n    this._version = version;\n  }\n\n  getComponent() {\n    return this._component;\n  }\n\n  receive(\n    request: {\n      query: PlainObject;\n      components?: PlainObject[];\n      version?: number;\n    },\n    options: {executionMode?: ExecutionMode} = {}\n  ) {\n    const {\n      query: serializedQuery,\n      components: serializedComponents,\n      version: clientVersion\n    } = request;\n\n    const {executionMode} = options;\n\n    this.validateVersion(clientVersion);\n\n    const componentFork = this._component.fork();\n\n    if (executionMode !== undefined) {\n      componentFork.setExecutionMode(executionMode);\n    }\n\n    const deeprRoot = this.getDeeprRoot();\n\n    const getFilter = function (attribute: Attribute) {\n      return attribute.operationIsAllowed('get');\n    };\n\n    const setFilter = function (attribute: Attribute) {\n      return executionMode === 'background' || attribute.operationIsAllowed('set');\n    };\n\n    const authorizer = function (this: any, name: string, operation: string, _params?: any[]) {\n      if (executionMode === 'background') {\n        return true;\n      }\n\n      if (this === deeprRoot && name === 'introspect' && operation === 'call') {\n        return true;\n      }\n\n      if (isComponentClassOrInstance(this)) {\n        const property = this.hasProperty(name) ? this.getProperty(name) : undefined;\n\n        if (property !== undefined) {\n          return property.operationIsAllowed(operation as PropertyOperation);\n        }\n      }\n\n      return false;\n    };\n\n    const errorHandler = function (error: Error) {\n      debugError(\n        `An error occurred while invoking a query (query: %o, components: %o)\\n%s`,\n        serializedQuery,\n        serializedComponents,\n        error.stack\n      );\n\n      if (executionMode === 'background') {\n        console.error(\n          `An error occurred while invoking a background query (query: ${JSON.stringify(\n            serializedQuery\n          )}): ${error.message}`\n        );\n      }\n\n      return error;\n    };\n\n    debugRequest({serializedQuery, serializedComponents});\n\n    return possiblyAsync(\n      this._deserializeRequest(\n        {serializedQuery, serializedComponents},\n        {rootComponent: componentFork, attributeFilter: setFilter}\n      ),\n      ({deserializedQuery, deserializedComponents}) =>\n        possiblyAsync(componentFork.initialize(), () =>\n          possiblyAsync(\n            invokeQuery(deeprRoot, deserializedQuery, {authorizer, errorHandler}),\n            (result) => {\n              if (executionMode === 'background') {\n                return {};\n              }\n\n              return possiblyAsync(\n                this._serializeResponse(\n                  {result, components: deserializedComponents},\n                  {attributeFilter: getFilter}\n                ),\n                ({serializedResult, serializedComponents}) => {\n                  debugResponse({serializedResult, serializedComponents});\n\n                  return {\n                    ...(typeof serializedResult !== 'undefined' && {result: serializedResult}),\n                    ...(typeof serializedComponents !== 'undefined' && {\n                      components: serializedComponents\n                    })\n                  };\n                }\n              );\n            }\n          )\n        )\n    );\n  }\n\n  _deserializeRequest(\n    {\n      serializedQuery,\n      serializedComponents\n    }: {serializedQuery: PlainObject; serializedComponents: PlainObject[] | undefined},\n    {\n      rootComponent,\n      attributeFilter\n    }: {rootComponent: typeof Component; attributeFilter: PropertyFilter}\n  ) {\n    return possiblyAsync(\n      deserialize(serializedComponents, {\n        rootComponent,\n        attributeFilter,\n        source: 'client'\n      }),\n      (deserializedComponents: (typeof Component | Component)[] | undefined) => {\n        const deserializedComponentSet: ComponentSet = new Set(deserializedComponents);\n        return possiblyAsync(\n          deserialize(serializedQuery, {\n            rootComponent,\n            attributeFilter,\n            deserializedComponents: deserializedComponentSet,\n            source: 'client'\n          }),\n          (deserializedQuery: PlainObject) => {\n            deserializedComponents = Array.from(deserializedComponentSet);\n            return {deserializedQuery, deserializedComponents};\n          }\n        );\n      }\n    );\n  }\n\n  _serializeResponse(\n    {\n      result,\n      components\n    }: {result: unknown; components: (typeof Component | Component)[] | undefined},\n    {attributeFilter}: {attributeFilter: PropertyFilter}\n  ) {\n    const serializedComponents: ComponentSet = new Set();\n    const componentDependencies: ComponentSet = new Set(components);\n    const possiblyAsyncSerializedResult =\n      typeof result !== 'undefined'\n        ? serialize(result, {\n            attributeFilter,\n            serializedComponents,\n            componentDependencies,\n            serializeFunctions: true,\n            target: 'client'\n          })\n        : undefined;\n\n    return possiblyAsync(possiblyAsyncSerializedResult, (serializedResult) => {\n      let serializedComponentDependencies: PlainObject[] | undefined;\n      const handledComponentDependencies = new Set(serializedComponents);\n\n      const serializeComponentDependencies = function (\n        componentDependencies: ComponentSet\n      ): void | PromiseLike<void> {\n        if (componentDependencies.size === 0) {\n          return;\n        }\n\n        const additionalComponentDependencies: ComponentSet = new Set();\n\n        return possiblyAsync(\n          possiblyAsync.forEach(\n            componentDependencies.values(),\n            (componentDependency: typeof Component | Component) => {\n              if (handledComponentDependencies.has(componentDependency)) {\n                return;\n              }\n\n              return possiblyAsync(\n                componentDependency.serialize({\n                  attributeFilter,\n                  componentDependencies: additionalComponentDependencies,\n                  ignoreEmptyComponents: true,\n                  serializeFunctions: true,\n                  target: 'client'\n                }),\n                (serializedComponentDependency) => {\n                  if (serializedComponentDependency !== undefined) {\n                    if (serializedComponentDependencies === undefined) {\n                      serializedComponentDependencies = [];\n                    }\n\n                    serializedComponentDependencies.push(serializedComponentDependency);\n                  }\n\n                  handledComponentDependencies.add(componentDependency);\n                }\n              );\n            }\n          ),\n          () => serializeComponentDependencies(additionalComponentDependencies)\n        );\n      };\n\n      return possiblyAsync(serializeComponentDependencies(componentDependencies), () => ({\n        serializedResult,\n        serializedComponents: serializedComponentDependencies\n      }));\n    });\n  }\n\n  validateVersion(clientVersion: number | undefined) {\n    const serverVersion = this._version;\n\n    if (clientVersion !== serverVersion) {\n      throw Object.assign(\n        new Error(\n          `The component client version (${clientVersion}) doesn't match the component server version (${serverVersion})`\n        ),\n        {code: 'COMPONENT_CLIENT_VERSION_DOES_NOT_MATCH_COMPONENT_SERVER_VERSION', expose: true}\n      );\n    }\n  }\n\n  _deeprRoot!: PlainObject;\n\n  getDeeprRoot() {\n    if (this._deeprRoot === undefined) {\n      this._deeprRoot = Object.create(null);\n\n      this._deeprRoot.introspect = () => ({\n        ...(this._name !== undefined && {name: this._name}),\n        component: this._introspectedComponent\n      });\n    }\n\n    return this._deeprRoot;\n  }\n\n  static isComponentServer(value: any): value is ComponentServer {\n    return isComponentServerInstance(value);\n  }\n}\n\nexport function ensureComponentServer(\n  componentOrComponentServer: typeof Component | ComponentServer,\n  options: ComponentServerOptions = {}\n): ComponentServer {\n  if (isComponentServerInstance(componentOrComponentServer)) {\n    return componentOrComponentServer;\n  }\n\n  return new ComponentServer(componentOrComponentServer, options);\n}\n\nfunction debugRequest({\n  serializedQuery,\n  serializedComponents\n}: {\n  serializedQuery: PlainObject;\n  serializedComponents: PlainObject[] | undefined;\n}) {\n  let message = 'Receiving query: %o';\n  const values = [serializedQuery];\n\n  if (serializedComponents !== undefined) {\n    message += ' (components: %o)';\n    values.push(serializedComponents);\n  }\n\n  debug(message, ...values);\n}\n\nfunction debugResponse({\n  serializedResult,\n  serializedComponents\n}: {\n  serializedResult: unknown;\n  serializedComponents: PlainObject[] | undefined;\n}) {\n  let message = 'Returning result: %o';\n  const values = [serializedResult];\n\n  if (serializedComponents !== undefined) {\n    message += ' (components: %o)';\n    values.push(serializedComponents);\n  }\n\n  debug(message, ...values);\n}\n"
  },
  {
    "path": "packages/component-server/src/index.ts",
    "content": "export * from './component-server';\nexport * from './utilities';\n"
  },
  {
    "path": "packages/component-server/src/utilities.ts",
    "content": "import {getTypeOf} from 'core-helpers';\n\nimport type {ComponentServer} from './component-server';\n\nexport function isComponentServerClass(value: any): value is typeof ComponentServer {\n  return typeof value?.isComponentServer === 'function';\n}\n\nexport function isComponentServerInstance(value: any): value is ComponentServer {\n  return typeof value?.constructor?.isComponentServer === 'function';\n}\n\nexport function assertIsComponentServerInstance(value: any): asserts value is ComponentServer {\n  if (!isComponentServerInstance(value)) {\n    throw new Error(\n      `Expected a component server instance, but received a value of type '${getTypeOf(value)}'`\n    );\n  }\n}\n"
  },
  {
    "path": "packages/component-server/tsconfig.json",
    "content": "{\n  \"extends\": \"@mvila/tsconfig\",\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/execution-queue/README.md",
    "content": "# @layr/store\n\nA base class for implementing Layr stores.\n\n## Installation\n\n```\nnpm install @layr/store\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/execution-queue/package.json",
    "content": "{\n  \"name\": \"@layr/execution-queue\",\n  \"version\": \"2.0.37\",\n  \"description\": \"A class for queueing method executions\",\n  \"keywords\": [\n    \"layr\",\n    \"method\",\n    \"queue\",\n    \"execution\",\n    \"queueing\"\n  ],\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/layrjs/layr/tree/master/packages/execution-queue\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"dist/node-cjs/index.js\",\n  \"module\": \"dist/node-esm/index.js\",\n  \"engines\": {\n    \"node\": \">=16.0.0\"\n  },\n  \"scripts\": {\n    \"build\": \"dev-tools build:ts-library\",\n    \"prepare\": \"npm run build\",\n    \"test\": \"dev-tools test:ts-library\",\n    \"publish:package\": \"dev-tools publish:package\",\n    \"update\": \"dev-tools update:dependencies\"\n  },\n  \"dependencies\": {\n    \"@layr/component\": \"^2.0.51\",\n    \"core-helpers\": \"^1.0.8\",\n    \"lodash\": \"^4.17.21\",\n    \"tslib\": \"^2.4.1\"\n  },\n  \"devDependencies\": {\n    \"@layr/component-server\": \"^2.0.70\",\n    \"@layr/utilities\": \"^1.0.9\",\n    \"@mvila/dev-tools\": \"^1.3.1\",\n    \"@mvila/tsconfig\": \"^1.0.6\",\n    \"@types/jest\": \"^29.2.5\",\n    \"@types/lodash\": \"^4.14.191\"\n  }\n}\n"
  },
  {
    "path": "packages/execution-queue/src/execution-queue.test.ts",
    "content": "import {Component, provide, primaryIdentifier, attribute, method} from '@layr/component';\nimport {ComponentServer} from '@layr/component-server';\nimport {sleep} from '@layr/utilities';\n\nimport {ExecutionQueue} from './execution-queue';\n\ndescribe('ExecutionQueue', () => {\n  test('Static method queuing', async () => {\n    let taskStatus: string | undefined = undefined;\n\n    class Application extends Component {\n      @method({queue: true}) static async task() {\n        taskStatus = 'running';\n        await sleep(150);\n        taskStatus = 'done';\n      }\n    }\n\n    const componentServer = new ComponentServer(Application);\n\n    const executionQueue = new ExecutionQueue(async (query) => {\n      await sleep(10);\n      componentServer.receive({query}, {executionMode: 'background'});\n    });\n\n    executionQueue.registerRootComponent(Application);\n\n    expect(taskStatus).toBeUndefined();\n\n    await Application.task();\n\n    expect(taskStatus).toBe('running');\n\n    await sleep(200);\n\n    expect(taskStatus).toBe('done');\n  });\n\n  test('Instance method queuing', async () => {\n    let movieStatus: string | undefined = undefined;\n\n    class Movie extends Component {\n      @primaryIdentifier() id!: string;\n\n      @attribute('string') title!: string;\n\n      @method({queue: true}) async play() {\n        movieStatus = `playing (id: '${this.id}')`;\n        await sleep(150);\n        movieStatus = `played (id: '${this.id}')`;\n      }\n    }\n\n    class Application extends Component {\n      @provide() static Movie = Movie;\n    }\n\n    const componentServer = new ComponentServer(Application);\n\n    const executionQueue = new ExecutionQueue(async (query) => {\n      await sleep(10);\n      componentServer.receive({query}, {executionMode: 'background'});\n    });\n\n    executionQueue.registerRootComponent(Application);\n\n    const movie = new Application.Movie({id: 'movie1', title: 'Inception'});\n\n    expect(movieStatus).toBeUndefined();\n\n    await movie.play();\n\n    expect(movieStatus).toBe(\"playing (id: 'movie1')\");\n\n    await sleep(200);\n\n    expect(movieStatus).toBe(\"played (id: 'movie1')\");\n  });\n});\n"
  },
  {
    "path": "packages/execution-queue/src/execution-queue.ts",
    "content": "import {Component, serialize, assertIsComponentClass} from '@layr/component';\nimport {hasOwnProperty, isPlainObject, PlainObject} from 'core-helpers';\n\ntype ExecutionQueueSender = (query: PlainObject) => Promise<any>;\n\nexport class ExecutionQueue {\n  _sender: ExecutionQueueSender;\n\n  constructor(sender: ExecutionQueueSender) {\n    this._sender = sender;\n  }\n\n  registerRootComponent(rootComponent: typeof Component) {\n    assertIsComponentClass(rootComponent);\n\n    const register = (component: typeof Component | Component) => {\n      for (const method of component.getMethods()) {\n        const name = method.getName();\n        const queueing = method.getQueueing();\n\n        if (!queueing) {\n          continue;\n        }\n\n        const orignalMethod: (...args: any[]) => any = (component as any)[name];\n\n        if (hasOwnProperty(orignalMethod, '__queued')) {\n          throw new Error(`Method already registered to an execution queue (${method.describe()})`);\n        }\n\n        const executionQueue = this;\n\n        const backgroundMethod = async function (\n          this: typeof Component | Component,\n          ...args: any[]\n        ) {\n          const [firstArgument, ...otherArguments] = args;\n\n          if (\n            isPlainObject(firstArgument) &&\n            hasOwnProperty(firstArgument, '__callOriginalMethod')\n          ) {\n            await orignalMethod.call(this, ...otherArguments);\n            return;\n          }\n\n          const query = {\n            '<=': this,\n            [`${name}=>`]: {'()': [{__callOriginalMethod: true}, ...args]}\n          };\n\n          const serializedQuery = serialize(query, {returnComponentReferences: true});\n\n          await executionQueue._sender(serializedQuery);\n        };\n\n        Object.defineProperty(backgroundMethod, '__queued', {value: true});\n\n        (component as any)[name] = backgroundMethod;\n      }\n    };\n\n    for (const component of rootComponent.traverseComponents()) {\n      if (!component.isEmbedded()) {\n        register(component);\n        register(component.prototype);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/execution-queue/src/index.ts",
    "content": "export * from './execution-queue';\n"
  },
  {
    "path": "packages/execution-queue/tsconfig.json",
    "content": "{\n  \"extends\": \"@mvila/tsconfig\",\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/integration-testing/README.md",
    "content": "# @layr/integration-testing\n\nTests involving a combination of Layr packages.\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/integration-testing/package.json",
    "content": "{\n  \"name\": \"@layr/integration-testing\",\n  \"version\": \"2.0.125\",\n  \"private\": true,\n  \"description\": \"Tests involving a combination of Layr packages\",\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/layrjs/layr/tree/master/packages/integration-testing\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"dist/node-cjs/index.js\",\n  \"module\": \"dist/node-esm/index.js\",\n  \"engines\": {\n    \"node\": \">=16.0.0\"\n  },\n  \"scripts\": {\n    \"prepare\": \"npm run test\",\n    \"test\": \"dev-tools test:ts-library\",\n    \"publish:package\": \"dev-tools publish:package\",\n    \"update\": \"dev-tools update:dependencies\"\n  },\n  \"dependencies\": {\n    \"@layr/component\": \"^2.0.51\",\n    \"@layr/component-client\": \"^2.0.86\",\n    \"@layr/component-express-middleware\": \"^2.0.94\",\n    \"@layr/component-http-client\": \"^2.0.76\",\n    \"@layr/component-http-server\": \"^2.0.97\",\n    \"@layr/component-koa-middleware\": \"^2.0.94\",\n    \"@layr/component-server\": \"^2.0.70\",\n    \"@layr/memory-navigator\": \"^2.0.59\",\n    \"@layr/memory-store\": \"^2.0.82\",\n    \"@layr/mongodb-store\": \"^2.0.82\",\n    \"@layr/navigator\": \"^2.0.55\",\n    \"@layr/routable\": \"^2.0.113\",\n    \"@layr/storable\": \"^2.0.76\",\n    \"@layr/store\": \"^2.0.81\",\n    \"cross-fetch\": \"^3.1.5\",\n    \"express\": \"^4.18.2\",\n    \"koa\": \"^2.14.1\",\n    \"koa-mount\": \"^4.0.0\",\n    \"lodash\": \"^4.17.21\",\n    \"mongodb\": \"^3.7.3\",\n    \"mongodb-memory-server\": \"^8.11.0\",\n    \"tslib\": \"^2.4.1\"\n  },\n  \"devDependencies\": {\n    \"@mvila/dev-tools\": \"^1.3.1\",\n    \"@mvila/tsconfig\": \"^1.0.6\",\n    \"@types/jest\": \"^29.2.5\",\n    \"@types/koa\": \"^2.13.5\",\n    \"@types/koa-mount\": \"^4.0.2\",\n    \"@types/lodash\": \"^4.14.191\",\n    \"@types/mongodb\": \"^3.6.20\"\n  }\n}\n"
  },
  {
    "path": "packages/integration-testing/src/client-server.test.ts",
    "content": "import {ComponentClient} from '@layr/component-client';\nimport {ComponentServer} from '@layr/component-server';\n\nimport {Counter as BackendCounter} from './counter.fixture';\n\ndescribe('Client/server', () => {\n  test('Simple component', async () => {\n    const server = new ComponentServer(BackendCounter);\n    const client = new ComponentClient(server);\n\n    const Counter = client.getComponent() as typeof BackendCounter;\n\n    let counter = new Counter();\n\n    expect(counter.value).toBe(0);\n\n    counter.increment();\n\n    expect(counter.value).toBe(1);\n\n    counter.increment();\n\n    expect(counter.value).toBe(2);\n  });\n});\n"
  },
  {
    "path": "packages/integration-testing/src/component-express-middleware.test.ts",
    "content": "import express from 'express';\nimport type {Server} from 'http';\nimport {serveComponent} from '@layr/component-express-middleware';\nimport {ComponentHTTPClient} from '@layr/component-http-client';\nimport fetch from 'cross-fetch';\n\nimport {Counter as BackendCounter} from './counter.fixture';\n\nconst SERVER_PORT = 6666;\n\ndescribe('Express middleware', () => {\n  let server: Server | undefined;\n\n  beforeAll(() => {\n    const app = express();\n\n    app.use('/api', serveComponent(BackendCounter));\n\n    return new Promise<void>((resolve) => {\n      server = app.listen(SERVER_PORT, resolve);\n    });\n  });\n\n  afterAll(() => {\n    server?.close();\n  });\n\n  test('API-less', async () => {\n    const client = new ComponentHTTPClient(`http://localhost:${SERVER_PORT}/api`);\n\n    const Counter = (await client.getComponent()) as typeof BackendCounter;\n\n    let counter = new Counter();\n\n    expect(counter.value).toBe(0);\n\n    await counter.increment();\n\n    expect(counter.value).toBe(1);\n\n    await counter.increment();\n\n    expect(counter.value).toBe(2);\n  });\n\n  test('REST API', async () => {\n    let response = await fetch(`http://localhost:${SERVER_PORT}/api/ping`);\n    let result = await response.json();\n\n    expect(result).toBe('pong');\n\n    response = await fetch(`http://localhost:${SERVER_PORT}/api/echo`, {\n      method: 'POST',\n      headers: {'Content-Type': 'application/json'},\n      body: JSON.stringify({message: 'hello'})\n    });\n    result = await response.json();\n\n    expect(result).toStrictEqual({message: 'hello'});\n\n    response = await fetch(`http://localhost:${SERVER_PORT}/api/echo`, {\n      method: 'POST',\n      headers: {'Content-Type': 'text/plain'},\n      body: 'hello'\n    });\n    result = await response.json();\n\n    expect(result).toBe('hello');\n\n    response = await fetch(`http://localhost:${SERVER_PORT}/api/echo`, {\n      method: 'POST',\n      body: Buffer.from([1, 2, 3])\n    });\n    result = await response.json();\n\n    expect(result).toStrictEqual([1, 2, 3]);\n  });\n});\n"
  },
  {
    "path": "packages/integration-testing/src/component-koa-middleware.test.ts",
    "content": "import Koa from 'koa';\nimport mount from 'koa-mount';\nimport type {Server} from 'http';\nimport {serveComponent} from '@layr/component-koa-middleware';\nimport {ComponentHTTPClient} from '@layr/component-http-client';\nimport fetch from 'cross-fetch';\n\nimport {Counter as BackendCounter} from './counter.fixture';\n\nconst SERVER_PORT = 5555;\n\ndescribe('Koa middleware', () => {\n  let server: Server | undefined;\n\n  beforeAll(() => {\n    const app = new Koa();\n\n    app.use(mount('/api', serveComponent(BackendCounter)));\n\n    return new Promise<void>((resolve) => {\n      server = app.listen(SERVER_PORT, resolve);\n    });\n  });\n\n  afterAll(() => {\n    server?.close();\n  });\n\n  test('API-less', async () => {\n    const client = new ComponentHTTPClient(`http://localhost:${SERVER_PORT}/api`);\n\n    const Counter = (await client.getComponent()) as typeof BackendCounter;\n\n    let counter = new Counter();\n\n    expect(counter.value).toBe(0);\n\n    await counter.increment();\n\n    expect(counter.value).toBe(1);\n\n    await counter.increment();\n\n    expect(counter.value).toBe(2);\n  });\n\n  test('REST API', async () => {\n    let response = await fetch(`http://localhost:${SERVER_PORT}/api/ping`);\n    let result = await response.json();\n\n    expect(result).toBe('pong');\n\n    response = await fetch(`http://localhost:${SERVER_PORT}/api/echo`, {\n      method: 'POST',\n      headers: {'Content-Type': 'application/json'},\n      body: JSON.stringify({message: 'hello'})\n    });\n    result = await response.json();\n\n    expect(result).toStrictEqual({message: 'hello'});\n\n    response = await fetch(`http://localhost:${SERVER_PORT}/api/echo`, {\n      method: 'POST',\n      headers: {'Content-Type': 'text/plain'},\n      body: 'hello'\n    });\n    result = await response.json();\n\n    expect(result).toBe('hello');\n\n    response = await fetch(`http://localhost:${SERVER_PORT}/api/echo`, {\n      method: 'POST',\n      body: Buffer.from([1, 2, 3])\n    });\n    result = await response.json();\n\n    expect(result).toStrictEqual([1, 2, 3]);\n  });\n});\n"
  },
  {
    "path": "packages/integration-testing/src/counter.fixture.ts",
    "content": "import {Component, primaryIdentifier, attribute, method, expose} from '@layr/component';\nimport {Routable, httpRoute} from '@layr/routable';\nimport {MemoryNavigator} from '@layr/memory-navigator';\n\nexport class Counter extends Routable(Component) {\n  @expose({get: true, set: true}) @primaryIdentifier() id!: string;\n\n  @expose({get: true, set: true}) @attribute('number') value = 0;\n\n  @expose({call: true}) @method() increment() {\n    this.value++;\n  }\n\n  @httpRoute('GET', '/ping') static ping() {\n    return 'pong';\n  }\n\n  @httpRoute('POST', '/echo', {\n    transformers: {\n      input(_, {headers, body}) {\n        let data;\n\n        if (Buffer.isBuffer(body)) {\n          data = Array.from(body);\n        } else if (headers['content-type'] === 'application/json') {\n          data = JSON.parse(body);\n        } else {\n          data = body;\n        }\n\n        return {data};\n      }\n    }\n  })\n  static echo({data}: {data: any}) {\n    return data;\n  }\n}\n\nconst navigator = new MemoryNavigator();\n\nCounter.registerNavigator(navigator);\n"
  },
  {
    "path": "packages/integration-testing/src/http-client-server.test.ts",
    "content": "import {ComponentHTTPClient} from '@layr/component-http-client';\nimport {ComponentHTTPServer} from '@layr/component-http-server';\n\nimport {Counter as BackendCounter} from './counter.fixture';\n\nconst SERVER_PORT = Math.floor(Math.random() * (60000 - 50000 + 1) + 50000);\n\ndescribe('HTTP client/server', () => {\n  let server: ComponentHTTPServer;\n\n  beforeAll(async () => {\n    server = new ComponentHTTPServer(BackendCounter, {port: SERVER_PORT});\n    await server.start();\n  });\n\n  afterAll(async () => {\n    await server?.stop();\n  });\n\n  test('Simple component', async () => {\n    const client = new ComponentHTTPClient(`http://localhost:${SERVER_PORT}`);\n\n    const Counter = (await client.getComponent()) as typeof BackendCounter;\n\n    let counter = new Counter();\n\n    expect(counter.value).toBe(0);\n\n    await counter.increment();\n\n    expect(counter.value).toBe(1);\n\n    await counter.increment();\n\n    expect(counter.value).toBe(2);\n  });\n});\n"
  },
  {
    "path": "packages/integration-testing/src/memory-navigator.test.ts",
    "content": "import {Component, provide, primaryIdentifier} from '@layr/component';\nimport {MemoryNavigator} from '@layr/memory-navigator';\nimport {Routable, route, wrapper, callRouteByURL} from '@layr/routable';\n\ndescribe('MemoryNavigator', () => {\n  let currentRouteResult: string;\n\n  const getNavigator = function () {\n    class Home extends Routable(Component) {\n      @route('[]/') static HomePage() {\n        return `Home`;\n      }\n    }\n\n    class Movie extends Routable(Component) {\n      @primaryIdentifier() id!: string;\n\n      @route('[/]movies') static ListPage() {\n        return `Movies`;\n      }\n\n      @wrapper('[/]movies/:id') ItemLayout({children}: {children: () => any}) {\n        return `Movie #${this.id}${children()}`;\n      }\n\n      @route('[/movies/:id]') ItemPage() {\n        return '';\n      }\n\n      @route('[/movies/:id]/details') DetailsPage() {\n        return ' (details)';\n      }\n    }\n\n    class Application extends Routable(Component) {\n      @provide() static Home = Home;\n      @provide() static Movie = Movie;\n\n      @wrapper('') static RootLayout({children}: {children: () => any}) {\n        return `{${children()}}`;\n      }\n\n      @wrapper('[]/') static MainLayout({children}: {children: () => any}) {\n        return `[${children()}]`;\n      }\n    }\n\n    const navigator = new MemoryNavigator({\n      initialURLs: ['/', '/movies', '/movies/abc123?showTrailers=true#main']\n    });\n\n    Application.registerNavigator(navigator);\n\n    navigator.addObserver(() => {\n      currentRouteResult = callRouteByURL(Application, navigator.getCurrentURL());\n    });\n\n    navigator.callObservers();\n\n    return navigator;\n  };\n\n  test('new ()', async () => {\n    let navigator = new MemoryNavigator();\n\n    expect(navigator.getHistoryLength()).toBe(0);\n\n    expect(() => navigator.getCurrentURL()).toThrow('The navigator has no current URL');\n\n    navigator = new MemoryNavigator({initialURLs: ['/', '/movies']});\n\n    expect(navigator.getHistoryLength()).toBe(2);\n    expect(navigator.getCurrentURL()).toBe('/movies');\n\n    navigator = new MemoryNavigator({initialURLs: ['/', '/movies'], initialIndex: 0});\n\n    expect(navigator.getCurrentURL()).toBe('/');\n  });\n\n  test('getCurrentURL()', async () => {\n    const navigator = getNavigator();\n\n    expect(navigator.getCurrentURL()).toBe('/movies/abc123?showTrailers=true#main');\n  });\n\n  test('getCurrentPath()', async () => {\n    const navigator = getNavigator();\n\n    expect(navigator.getCurrentPath()).toBe('/movies/abc123');\n\n    navigator.goBack({defer: false});\n\n    expect(navigator.getCurrentPath()).toBe('/movies');\n  });\n\n  test('getCurrentQuery()', async () => {\n    const navigator = getNavigator();\n\n    expect(navigator.getCurrentQuery()).toEqual({showTrailers: 'true'});\n\n    navigator.goBack({defer: false});\n\n    expect(navigator.getCurrentQuery()).toEqual({});\n  });\n\n  test('getCurrentHash()', async () => {\n    const navigator = getNavigator();\n\n    expect(navigator.getCurrentHash()).toBe('main');\n\n    navigator.goBack({defer: false});\n\n    expect(navigator.getCurrentHash()).toBeUndefined();\n  });\n\n  test('navigate()', async () => {\n    const navigator = getNavigator();\n\n    expect(currentRouteResult).toBe('{[Movie #abc123]}');\n    expect(navigator.getHistoryLength()).toBe(3);\n\n    navigator.navigate('/movies/abc123/details', {defer: false});\n\n    expect(currentRouteResult).toBe('{[Movie #abc123 (details)]}');\n    expect(navigator.getHistoryLength()).toBe(4);\n\n    navigator.go(-3); // We should be at the first entry of the history\n\n    navigator.navigate('/movies/abc123', {defer: false});\n\n    expect(currentRouteResult).toBe('{[Movie #abc123]}');\n    expect(navigator.getHistoryLength()).toBe(2);\n    expect(navigator.getCurrentQuery()).toEqual({});\n\n    navigator.navigate('/movies/abc123?showTrailers=true', {defer: false});\n\n    expect(currentRouteResult).toBe('{[Movie #abc123]}');\n    expect(navigator.getHistoryLength()).toBe(3);\n    expect(navigator.getCurrentQuery()).toEqual({showTrailers: 'true'});\n  });\n\n  test('redirect()', async () => {\n    const navigator = getNavigator();\n\n    expect(currentRouteResult).toBe('{[Movie #abc123]}');\n    expect(navigator.getHistoryLength()).toBe(3);\n\n    navigator.redirect('/movies/def456', {defer: false});\n\n    expect(currentRouteResult).toBe('{[Movie #def456]}');\n    expect(navigator.getHistoryLength()).toBe(3);\n\n    navigator.go(-2); // We should be at the first entry of the history\n\n    navigator.redirect('/movies/abc123', {defer: false});\n\n    expect(currentRouteResult).toBe('{[Movie #abc123]}');\n    expect(navigator.getHistoryLength()).toBe(1);\n  });\n\n  test('go()', async () => {\n    const navigator = getNavigator();\n\n    expect(currentRouteResult).toBe('{[Movie #abc123]}');\n\n    navigator.go(-1, {defer: false});\n\n    expect(currentRouteResult).toBe('{[Movies]}');\n\n    navigator.go(-1, {defer: false});\n\n    expect(currentRouteResult).toBe('{Home}');\n\n    navigator.go(2, {defer: false});\n\n    expect(currentRouteResult).toBe('{[Movie #abc123]}');\n\n    expect(() => navigator.go(1, {defer: false})).toThrow(\n      'Cannot go to an entry that does not exist in the navigator history'\n    );\n\n    expect(currentRouteResult).toBe('{[Movie #abc123]}');\n\n    expect(() => navigator.go(2, {defer: false})).toThrow(\n      'Cannot go to an entry that does not exist in the navigator history'\n    );\n\n    expect(currentRouteResult).toBe('{[Movie #abc123]}');\n\n    expect(() => navigator.go(-3, {defer: false})).toThrow(\n      'Cannot go to an entry that does not exist in the navigator history'\n    );\n\n    expect(currentRouteResult).toBe('{[Movie #abc123]}');\n\n    expect(() => navigator.go(-4, {defer: false})).toThrow(\n      'Cannot go to an entry that does not exist in the navigator history'\n    );\n  });\n\n  test('goBack()', async () => {\n    const navigator = getNavigator();\n\n    expect(currentRouteResult).toBe('{[Movie #abc123]}');\n\n    navigator.goBack({defer: false});\n\n    expect(currentRouteResult).toBe('{[Movies]}');\n\n    navigator.goBack({defer: false});\n\n    expect(currentRouteResult).toBe('{Home}');\n\n    expect(() => navigator.goBack({defer: false})).toThrow(\n      'Cannot go to an entry that does not exist in the navigator history'\n    );\n\n    expect(currentRouteResult).toBe('{Home}');\n  });\n\n  test('goForward()', async () => {\n    const navigator = getNavigator();\n\n    navigator.go(-2, {defer: false});\n\n    expect(currentRouteResult).toBe('{Home}');\n\n    navigator.goForward({defer: false});\n\n    expect(currentRouteResult).toBe('{[Movies]}');\n\n    navigator.goForward({defer: false});\n\n    expect(currentRouteResult).toBe('{[Movie #abc123]}');\n\n    expect(() => navigator.goForward({defer: false})).toThrow(\n      'Cannot go to an entry that does not exist in the navigator history'\n    );\n\n    expect(currentRouteResult).toBe('{[Movie #abc123]}');\n  });\n\n  test('getHistoryLength()', async () => {\n    const navigator = getNavigator();\n\n    expect(navigator.getHistoryLength()).toBe(3);\n  });\n});\n"
  },
  {
    "path": "packages/integration-testing/src/navigator.test.ts",
    "content": "import {Component, provide, primaryIdentifier} from '@layr/component';\nimport {Routable, route, wrapper, findRouteByURL, callRouteByURL} from '@layr/routable';\nimport {Navigator, isNavigatorInstance} from '@layr/navigator';\n\ndescribe('Navigator', () => {\n  class MockNavigator extends Navigator {\n    _getCurrentURL() {\n      return new URL('http://localhost/');\n    }\n\n    _navigate(_url: URL) {}\n\n    _redirect(_url: URL) {}\n\n    _reload(_url: URL | undefined) {}\n\n    _go(_delta: number) {}\n\n    _getHistoryLength(): number {\n      return 1;\n    }\n\n    _getHistoryIndex(): number {\n      return 0;\n    }\n  }\n\n  test('Creation', async () => {\n    let navigator = new MockNavigator();\n\n    expect(isNavigatorInstance(navigator)).toBe(true);\n\n    // @ts-expect-error\n    expect(() => new MockNavigator({unknown: true})).toThrow(\n      \"Did not expect the option 'unknown' to exist\"\n    );\n  });\n\n  describe('Registration', () => {\n    test('registerNavigator() and getNavigator()', async () => {\n      class Profile extends Routable(Component) {}\n\n      class User extends Routable(Component) {\n        @provide() static Profile = Profile;\n      }\n\n      class Movie extends Routable(Component) {}\n\n      class Application extends Routable(Component) {\n        @provide() static User = User;\n        @provide() static Movie = Movie;\n      }\n\n      const navigator = new MockNavigator();\n\n      Application.registerNavigator(navigator);\n\n      expect(Application.getNavigator()).toBe(navigator);\n      expect(User.getNavigator()).toBe(navigator);\n      expect(Movie.getNavigator()).toBe(navigator);\n      expect(Profile.getNavigator()).toBe(navigator);\n\n      expect(Application.prototype.getNavigator()).toBe(navigator);\n      expect(User.prototype.getNavigator()).toBe(navigator);\n      expect(Movie.prototype.getNavigator()).toBe(navigator);\n      expect(Profile.prototype.getNavigator()).toBe(navigator);\n    });\n  });\n\n  describe('Routes', () => {\n    const getApplication = function () {\n      class Movie extends Routable(Component) {\n        @primaryIdentifier() id!: string;\n\n        @route('[/]movies/:id', {params: {showDetails: 'boolean?'}}) ItemPage({\n          showDetails = false\n        }) {\n          return `Movie #${this.id}${showDetails ? ' (with details)' : ''}`;\n        }\n      }\n\n      class Actor extends Routable(Component) {\n        @route('[/]actors/top') static TopPage() {\n          return 'Top actors';\n        }\n      }\n\n      class Application extends Routable(Component) {\n        @provide() static Movie = Movie;\n        @provide() static Actor = Actor;\n\n        @wrapper('/') static MainLayout({children}: {children: () => any}) {\n          return `[${children()}]`;\n        }\n\n        @route('[/]ping', {\n          filter(request) {\n            return request?.method === 'GET';\n          }\n        })\n        static ping() {\n          return 'pong';\n        }\n\n        @route('[/]*') static NotFoundPage() {\n          return 'Sorry, there is nothing here.';\n        }\n      }\n\n      const navigator = new MockNavigator();\n\n      Application.registerNavigator(navigator);\n\n      return Application;\n    };\n\n    test('findRouteByURL()', async () => {\n      const Application = getApplication();\n\n      let result = findRouteByURL(Application, '/movies/abc123');\n\n      expect(result!.routable.getComponentType()).toBe('Movie');\n      expect(result!.route.getName()).toBe('ItemPage');\n      expect(result!.identifiers).toEqual({id: 'abc123'});\n      expect(result!.params).toEqual({});\n\n      result = findRouteByURL(Application, '/movies/abc123?showDetails=1');\n\n      expect(result!.routable.getComponentType()).toBe('Movie');\n      expect(result!.route.getName()).toBe('ItemPage');\n      expect(result!.identifiers).toEqual({id: 'abc123'});\n      expect(result!.params).toEqual({showDetails: true});\n\n      result = findRouteByURL(Application, '/actors/top');\n\n      expect(result!.routable.getComponentType()).toBe('typeof Actor');\n      expect(result!.route.getName()).toBe('TopPage');\n      expect(result!.identifiers).toEqual({});\n      expect(result!.params).toEqual({});\n\n      result = findRouteByURL(Application, '/movies/abc123/details');\n\n      expect(result!.routable.getComponentType()).toBe('typeof Application');\n      expect(result!.route.getName()).toBe('NotFoundPage');\n      expect(result!.identifiers).toEqual({});\n      expect(result!.params).toEqual({});\n\n      result = findRouteByURL(Application, '/ping', {method: 'GET'});\n\n      expect(result!.routable.getComponentType()).toBe('typeof Application');\n      expect(result!.route.getName()).toBe('ping');\n      expect(result!.identifiers).toEqual({});\n      expect(result!.params).toEqual({});\n\n      result = findRouteByURL(Application, '/ping', {method: 'POST'});\n\n      expect(result!.routable.getComponentType()).toBe('typeof Application');\n      expect(result!.route.getName()).toBe('NotFoundPage');\n      expect(result!.identifiers).toEqual({});\n      expect(result!.params).toEqual({});\n    });\n\n    test('callRouteByURL()', async () => {\n      const Application = getApplication();\n\n      expect(callRouteByURL(Application, '/movies/abc123')).toBe('[Movie #abc123]');\n      expect(callRouteByURL(Application, '/movies/abc123?showDetails=1')).toBe(\n        '[Movie #abc123 (with details)]'\n      );\n      expect(callRouteByURL(Application, '/actors/top')).toBe('[Top actors]');\n      expect(callRouteByURL(Application, '/movies/abc123/details')).toBe(\n        '[Sorry, there is nothing here.]'\n      );\n      expect(callRouteByURL(Application, '/ping', {method: 'GET'})).toBe('[pong]');\n      expect(callRouteByURL(Application, '/ping', {method: 'POST'})).toBe(\n        '[Sorry, there is nothing here.]'\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "packages/integration-testing/src/storable.fixture.ts",
    "content": "import {MongoClient} from 'mongodb';\nimport mapKeys from 'lodash/mapKeys';\n\nexport const CREATED_ON = new Date('2020-03-22T01:27:42.612Z');\nexport const UPDATED_ON = new Date('2020-03-22T01:29:33.673Z');\n\nexport function getInitialCollections() {\n  return {\n    User: [\n      {\n        __component: 'User',\n        id: 'user1',\n        email: '1@user.com',\n        reference: 1,\n        fullName: 'User 1',\n        accessLevel: 0,\n        tags: ['spammer', 'blocked'],\n        location: {country: 'USA', city: 'Paris'},\n        pastLocations: [\n          {country: 'USA', city: 'Nice'},\n          {country: 'USA', city: 'New York'}\n        ],\n        picture: {__component: 'Picture', type: 'JPEG', url: 'https://pictures.com/1-2.jpg'},\n        pastPictures: [\n          {__component: 'Picture', type: 'JPEG', url: 'https://pictures.com/1-1.jpg'},\n          {__component: 'Picture', type: 'PNG', url: 'https://pictures.com/1-1.png'}\n        ],\n        organization: {__component: 'Organization', id: 'org1'},\n        emailIsVerified: false,\n        createdOn: CREATED_ON\n      },\n      {\n        __component: 'User',\n        id: 'user11',\n        email: '11@user.com',\n        reference: 11,\n        fullName: 'User 11',\n        accessLevel: 3,\n        tags: ['owner', 'admin'],\n        location: {country: 'USA'},\n        pastLocations: [{country: 'France'}],\n        picture: {__component: 'Picture', type: 'JPEG', url: 'https://pictures.com/11-1.jpg'},\n        pastPictures: [{__component: 'Picture', type: 'PNG', url: 'https://pictures.com/11-1.png'}],\n        organization: {__component: 'Organization', id: 'org2'},\n        emailIsVerified: true,\n        createdOn: CREATED_ON\n      },\n      {\n        __component: 'User',\n        id: 'user12',\n        email: '12@user.com',\n        reference: 12,\n        fullName: 'User 12',\n        accessLevel: 1,\n        tags: [],\n        location: {country: 'France', city: 'Paris'},\n        pastLocations: [\n          {country: 'France', city: 'Nice'},\n          {country: 'Japan', city: 'Tokyo'}\n        ],\n        picture: {__component: 'Picture', type: 'PNG', url: 'https://pictures.com/12-3.png'},\n        pastPictures: [\n          {__component: 'Picture', type: 'PNG', url: 'https://pictures.com/12-2.png'},\n          {__component: 'Picture', type: 'PNG', url: 'https://pictures.com/12-1.png'}\n        ],\n        organization: {__component: 'Organization', id: 'org2'},\n        emailIsVerified: true,\n        createdOn: CREATED_ON\n      },\n      {\n        __component: 'User',\n        id: 'user13',\n        email: '13@user.com',\n        reference: 13,\n        fullName: 'User 13',\n        accessLevel: 3,\n        tags: ['admin'],\n        pastLocations: [],\n        pastPictures: [],\n        emailIsVerified: false,\n        createdOn: CREATED_ON\n      }\n    ],\n    Organization: [\n      {\n        __component: 'Organization',\n        id: 'org1',\n        slug: 'organization-1',\n        name: 'Organization 1'\n      },\n      {\n        __component: 'Organization',\n        id: 'org2',\n        slug: 'organization-2',\n        name: 'Organization 2'\n      }\n    ]\n  };\n}\n\nexport async function seedMongoDB(connectionString: string) {\n  const client = await MongoClient.connect(connectionString, {\n    useNewUrlParser: true,\n    useUnifiedTopology: true\n  });\n\n  const initialCollections = getInitialCollections();\n\n  for (const [collectionName, serializedStorables] of Object.entries(initialCollections)) {\n    const collection = client.db().collection(collectionName);\n\n    for (const serializedStorable of serializedStorables) {\n      const document = mapKeys(serializedStorable, (_, name) => (name === 'id' ? '_id' : name));\n      await collection.insertOne(document);\n    }\n  }\n\n  await client.close();\n}\n"
  },
  {
    "path": "packages/integration-testing/src/storable.test.ts",
    "content": "import {\n  Component,\n  EmbeddedComponent,\n  AttributeSelector,\n  provide,\n  expose,\n  serialize\n} from '@layr/component';\nimport {\n  Storable,\n  StorableComponent,\n  attribute,\n  primaryIdentifier,\n  secondaryIdentifier,\n  method,\n  loader,\n  finder,\n  isStorableClass,\n  isStorableInstance,\n  StorableAttributeHookName\n} from '@layr/storable';\nimport type {Store} from '@layr/store';\nimport {MemoryStore} from '@layr/memory-store';\nimport {MongoDBStore} from '@layr/mongodb-store';\nimport {MongoMemoryServer} from 'mongodb-memory-server';\nimport {ComponentClient} from '@layr/component-client';\nimport {ComponentServer} from '@layr/component-server';\nimport {PlainObject} from 'core-helpers';\n\nimport {getInitialCollections, CREATED_ON, UPDATED_ON, seedMongoDB} from './storable.fixture';\n\njest.setTimeout(60 * 1000); // 1 minute\n\ndescribe('Storable', () => {\n  class BasePicture extends EmbeddedComponent {\n    @attribute('string?') type?: string;\n    @attribute('string') url!: string;\n  }\n\n  class BaseOrganization extends Storable(Component) {\n    @primaryIdentifier() id!: string;\n    @secondaryIdentifier() slug!: string;\n    @attribute('string') name!: string;\n  }\n\n  class BaseUser extends Storable(Component) {\n    @provide() static Picture = BasePicture;\n    @provide() static Organization = BaseOrganization;\n\n    @primaryIdentifier() id!: string;\n    @secondaryIdentifier() email!: string;\n    @secondaryIdentifier('number') reference!: number;\n    @attribute('string') fullName: string = '';\n    @attribute('number') accessLevel: number = 0;\n    @attribute('string[]') tags: string[] = [];\n    @attribute('object?') location?: PlainObject;\n    @attribute('object[]') pastLocations: PlainObject[] = [];\n    @attribute('Picture?') picture?: BasePicture;\n    @attribute('Picture[]') pastPictures: BasePicture[] = [];\n    @attribute('Organization?') organization?: BaseOrganization;\n    @attribute('boolean') emailIsVerified: boolean = false;\n    @attribute('Date') createdOn: Date = new Date('2020-03-22T01:27:42.612Z');\n    @attribute('Date?') updatedOn?: Date;\n  }\n\n  describe('General methods', () => {\n    test('isStorableClass()', async () => {\n      class Picture extends BasePicture {}\n\n      class Organization extends BaseOrganization {}\n\n      class User extends BaseUser {\n        @provide() static Picture = Picture;\n        @provide() static Organization = Organization;\n      }\n\n      expect(isStorableClass(User)).toBe(true);\n      expect(isStorableClass(User.prototype)).toBe(false);\n      expect(isStorableClass(Picture)).toBe(false);\n      expect(isStorableClass(Organization)).toBe(true);\n\n      const user = new User({id: 'user1', email: '1@user.com', reference: 1});\n\n      expect(isStorableClass(user)).toBe(false);\n    });\n\n    test('isStorableInstance()', async () => {\n      class Picture extends BasePicture {}\n\n      class Organization extends BaseOrganization {}\n\n      class User extends BaseUser {\n        @provide() static Picture = Picture;\n        @provide() static Organization = Organization;\n      }\n\n      expect(isStorableInstance(User.prototype)).toBe(true);\n      expect(isStorableInstance(User)).toBe(false);\n      expect(isStorableInstance(Picture.prototype)).toBe(false);\n      expect(isStorableInstance(Organization.prototype)).toBe(true);\n\n      const user = new User({\n        id: 'user1',\n        email: '1@user.com',\n        reference: 1,\n        picture: new Picture({type: 'JPEG', url: 'https://pictures.com/1-1.jpg'}),\n        organization: new Organization({id: 'org1', slug: 'organization-1', name: 'Organization 1'})\n      });\n\n      expect(isStorableInstance(user)).toBe(true);\n      expect(isStorableInstance(user.picture)).toBe(false);\n      expect(isStorableInstance(user.organization)).toBe(true);\n    });\n\n    test('getStore() and hasStore()', async () => {\n      class Picture extends BasePicture {}\n\n      class Organization extends BaseOrganization {}\n\n      class User extends BaseUser {\n        @provide() static Picture = Picture;\n        @provide() static Organization = Organization;\n      }\n\n      expect(User.hasStore()).toBe(false);\n      expect(() => User.getStore()).toThrow(\n        \"Cannot get the store of a storable component that is not registered (component: 'User')\"\n      );\n      expect(Organization.hasStore()).toBe(false);\n\n      const store = new MemoryStore();\n\n      store.registerRootComponent(User);\n\n      expect(User.hasStore()).toBe(true);\n      expect(User.getStore()).toBe(store);\n      expect(Organization.hasStore()).toBe(true);\n      expect(Organization.getStore()).toBe(store);\n    });\n  });\n\n  describe('Storage operations', () => {\n    if (true) {\n      describe('With a local memory store', () => {\n        testOperations(function () {\n          class Picture extends BasePicture {}\n\n          class Organization extends BaseOrganization {}\n\n          class User extends BaseUser {\n            @provide() static Picture = Picture;\n            @provide() static Organization = Organization;\n          }\n\n          const store = new MemoryStore({initialCollections: getInitialCollections()});\n\n          store.registerRootComponent(User);\n\n          return User;\n        });\n      });\n    }\n\n    if (true) {\n      describe('With a local MongoDB store', () => {\n        let userClass: typeof BaseUser;\n        let server: MongoMemoryServer;\n        let store: MongoDBStore;\n\n        beforeEach(async () => {\n          class Picture extends BasePicture {}\n\n          class Organization extends BaseOrganization {}\n\n          class User extends BaseUser {\n            @provide() static Picture = Picture;\n            @provide() static Organization = Organization;\n          }\n\n          userClass = User;\n\n          server = await MongoMemoryServer.create({instance: {storageEngine: 'wiredTiger'}});\n\n          const connectionString = server.getUri();\n\n          await seedMongoDB(connectionString);\n\n          store = new MongoDBStore(connectionString);\n\n          store.registerRootComponent(User);\n\n          await store.connect();\n\n          await store.migrateStorables({silent: true});\n        });\n\n        afterEach(async () => {\n          await store?.disconnect();\n\n          await server?.stop();\n        });\n\n        testOperations(function () {\n          return userClass;\n        });\n      });\n    }\n\n    if (true) {\n      describe('With a remote memory store', () => {\n        testOperations(async () => {\n          const server = (() => {\n            @expose({\n              prototype: {\n                type: {get: true, set: true},\n                url: {get: true, set: true}\n              }\n            })\n            class Picture extends BasePicture {}\n\n            @expose({\n              get: {call: true},\n\n              prototype: {\n                id: {get: true, set: true},\n                slug: {get: true},\n                name: {get: true, set: true},\n\n                load: {call: true}\n              }\n            })\n            class Organization extends BaseOrganization {}\n\n            @expose({\n              get: {call: true},\n              find: {call: true},\n              count: {call: true},\n\n              prototype: {\n                id: {get: true, set: true},\n                email: {get: true, set: true},\n                reference: {get: true, set: true},\n                fullName: {get: true, set: true},\n                accessLevel: {get: true, set: true},\n                tags: {get: true, set: true},\n                location: {get: true, set: true},\n                pastLocations: {get: true, set: true},\n                picture: {get: true, set: true},\n                pastPictures: {get: true, set: true},\n                organization: {get: true, set: true},\n                emailIsVerified: {get: true, set: true},\n                createdOn: {get: true, set: true},\n                updatedOn: {get: true, set: true},\n\n                load: {call: true},\n                save: {call: true},\n                delete: {call: true}\n              }\n            })\n            class User extends BaseUser {\n              @provide() static Picture = Picture;\n              @provide() static Organization = Organization;\n            }\n\n            const store = new MemoryStore({initialCollections: getInitialCollections()});\n\n            store.registerRootComponent(User);\n\n            return new ComponentServer(User);\n          })();\n\n          const client = new ComponentClient(server, {mixins: [Storable], batchable: true});\n          const User = (await client.getComponent()) as typeof BaseUser;\n\n          return User;\n        });\n      });\n    }\n\n    if (true) {\n      describe('Without a store', () => {\n        testOperations(function () {\n          class Picture extends BasePicture {}\n\n          class Organization extends BaseOrganization {}\n\n          class User extends BaseUser {\n            @provide() static Picture = Picture;\n            @provide() static Organization = Organization;\n          }\n\n          return User;\n        });\n      });\n    }\n\n    function testOperations(userClassProvider: () => typeof BaseUser | Promise<typeof BaseUser>) {\n      describe('Storable instances', () => {\n        test('get()', async () => {\n          const User = await userClassProvider();\n\n          if (!(User.hasStore() || User.getRemoteComponent() !== undefined)) {\n            await expect(User.fork().get({id: 'user1'})).rejects.toThrow(\n              \"To be able to execute the load() method (called from get()), a storable component should be registered in a store or have an exposed load() remote method (component: 'User')\"\n            );\n\n            await expect(User.fork().get({email: '1@user.com'})).rejects.toThrow(\n              \"To be able to execute the get() method with a secondary identifier, a storable component should be registered in a store or have an exposed get() remote method (component: 'User')\"\n            );\n\n            return;\n          }\n\n          let user = await User.fork().get('user1');\n\n          const expectedSerializedUser = {\n            __component: 'User',\n            id: 'user1',\n            email: '1@user.com',\n            reference: 1,\n            fullName: 'User 1',\n            accessLevel: 0,\n            tags: ['spammer', 'blocked'],\n            location: {country: 'USA', city: 'Paris'},\n            pastLocations: [\n              {country: 'USA', city: 'Nice'},\n              {country: 'USA', city: 'New York'}\n            ],\n            picture: {\n              __component: 'Picture',\n              type: 'JPEG',\n              url: 'https://pictures.com/1-2.jpg'\n            },\n            pastPictures: [\n              {\n                __component: 'Picture',\n                type: 'JPEG',\n                url: 'https://pictures.com/1-1.jpg'\n              },\n              {\n                __component: 'Picture',\n                type: 'PNG',\n                url: 'https://pictures.com/1-1.png'\n              }\n            ],\n            organization: {\n              __component: 'Organization',\n              id: 'org1',\n              slug: 'organization-1',\n              name: 'Organization 1'\n            },\n            emailIsVerified: false,\n            createdOn: {__date: CREATED_ON.toISOString()},\n            updatedOn: {__undefined: true}\n          };\n\n          expect(user.serialize({includeReferencedComponents: true})).toStrictEqual(\n            expectedSerializedUser\n          );\n\n          user = await User.fork().get({id: 'user1'});\n\n          expect(user.serialize({includeReferencedComponents: true})).toStrictEqual(\n            expectedSerializedUser\n          );\n\n          user = await User.fork().get({id: 'user1'}, {fullName: true});\n\n          expect(user.serialize()).toStrictEqual({\n            __component: 'User',\n            id: 'user1',\n            fullName: 'User 1'\n          });\n\n          user = await User.fork().get({id: 'user1'}, {});\n\n          expect(user.serialize()).toStrictEqual({__component: 'User', id: 'user1'});\n\n          await expect(User.fork().get({id: 'user2'})).rejects.toThrow(\n            \"Cannot load a component that is missing from the store (component: 'User', id: 'user2')\"\n          );\n\n          expect(\n            await User.fork().get({id: 'user2'}, true, {throwIfMissing: false})\n          ).toBeUndefined();\n\n          user = await User.fork().get({email: '1@user.com'});\n\n          expect(user.serialize({includeReferencedComponents: true})).toStrictEqual(\n            expectedSerializedUser\n          );\n\n          user = await User.fork().get({email: '1@user.com'}, {fullName: true});\n\n          expect(user.serialize()).toStrictEqual({\n            __component: 'User',\n            id: 'user1',\n            email: '1@user.com',\n            fullName: 'User 1'\n          });\n\n          await expect(User.fork().get({email: '2@user.com'})).rejects.toThrow(\n            \"Cannot load a component that is missing from the store (component: 'User', email: '2@user.com')\"\n          );\n\n          user = await User.fork().get({reference: 1}, {fullName: true});\n\n          expect(user.serialize()).toStrictEqual({\n            __component: 'User',\n            id: 'user1',\n            reference: 1,\n            fullName: 'User 1'\n          });\n\n          await expect(User.fork().get({reference: 2})).rejects.toThrow(\n            \"Cannot load a component that is missing from the store (component: 'User', reference: 2)\"\n          );\n\n          await expect(User.fork().get({fullName: 'User 1'})).rejects.toThrow(\n            \"A property with the specified name was found, but it is not an identifier attribute (attribute: 'User.prototype.fullName')\"\n          );\n\n          await expect(User.fork().get({name: 'User 1'})).rejects.toThrow(\n            \"The identifier attribute 'name' is missing (component: 'User')\"\n          );\n\n          const UserFork = User.fork();\n\n          user = new UserFork({id: 'user1', email: '1@user.com', reference: 1});\n\n          await expect(UserFork.get({id: 'user1'})).rejects.toThrow(\n            \"Cannot load a storable component that is marked as new (component: 'User')\"\n          );\n        });\n\n        test('has()', async () => {\n          const User = await userClassProvider();\n\n          if (!(User.hasStore() || User.getRemoteComponent() !== undefined)) {\n            await expect(User.fork().has('user1')).rejects.toThrow(\n              \"To be able to execute the load() method (called from has()), a storable component should be registered in a store or have an exposed load() remote method (component: 'User')\"\n            );\n\n            await expect(User.fork().has({email: '1@user.com'})).rejects.toThrow(\n              \"To be able to execute the get() method (called from has()) with a secondary identifier, a storable component should be registered in a store or have an exposed get() remote method (component: 'User')\"\n            );\n\n            return;\n          }\n\n          expect(await User.fork().has('user1')).toBe(true);\n          expect(await User.fork().has('user2')).toBe(false);\n\n          expect(await User.fork().has({id: 'user1'})).toBe(true);\n          expect(await User.fork().has({id: 'user2'})).toBe(false);\n\n          expect(await User.fork().has({email: '1@user.com'})).toBe(true);\n          expect(await User.fork().has({email: '2@user.com'})).toBe(false);\n\n          expect(await User.fork().has({reference: 1})).toBe(true);\n          expect(await User.fork().has({reference: 2})).toBe(false);\n        });\n\n        test('load()', async () => {\n          const User = await userClassProvider();\n\n          let UserFork: typeof BaseUser;\n          let user: BaseUser;\n          let organization: BaseOrganization;\n\n          user = User.fork().instantiate({id: 'user1'});\n\n          if (!(User.hasStore() || User.getRemoteComponent() !== undefined)) {\n            return await expect(user.load()).rejects.toThrow(\n              \"To be able to execute the load() method, a storable component should be registered in a store or have an exposed load() remote method (component: 'User')\"\n            );\n          }\n\n          expect(await user.load({})).toBe(user);\n          expect(user.serialize()).toStrictEqual({__component: 'User', id: 'user1'});\n\n          expect(await user.load({email: true})).toBe(user);\n          expect(user.serialize()).toStrictEqual({\n            __component: 'User',\n            id: 'user1',\n            email: '1@user.com'\n          });\n\n          expect(await user.load()).toBe(user);\n          expect(user.serialize({includeReferencedComponents: true})).toStrictEqual({\n            __component: 'User',\n            id: 'user1',\n            email: '1@user.com',\n            reference: 1,\n            fullName: 'User 1',\n            accessLevel: 0,\n            tags: ['spammer', 'blocked'],\n            location: {country: 'USA', city: 'Paris'},\n            pastLocations: [\n              {country: 'USA', city: 'Nice'},\n              {country: 'USA', city: 'New York'}\n            ],\n            picture: {\n              __component: 'Picture',\n              type: 'JPEG',\n              url: 'https://pictures.com/1-2.jpg'\n            },\n            pastPictures: [\n              {\n                __component: 'Picture',\n                type: 'JPEG',\n                url: 'https://pictures.com/1-1.jpg'\n              },\n              {\n                __component: 'Picture',\n                type: 'PNG',\n                url: 'https://pictures.com/1-1.png'\n              }\n            ],\n            organization: {\n              __component: 'Organization',\n              id: 'org1',\n              slug: 'organization-1',\n              name: 'Organization 1'\n            },\n            emailIsVerified: false,\n            createdOn: {__date: CREATED_ON.toISOString()},\n            updatedOn: {__undefined: true}\n          });\n\n          // ------\n\n          user = User.fork().instantiate({id: 'user2'});\n\n          await expect(user.load({})).rejects.toThrow(\n            \"Cannot load a component that is missing from the store (component: 'User', id: 'user2'\"\n          );\n          expect(await user.load({}, {throwIfMissing: false})).toBeUndefined();\n\n          // ------\n\n          user = User.fork().instantiate({email: '1@user.com'});\n\n          expect(await user.load({})).toBe(user);\n          expect(user.serialize()).toStrictEqual({\n            __component: 'User',\n            id: 'user1',\n            email: '1@user.com'\n          });\n\n          expect(await user.load({fullName: true})).toBe(user);\n          expect(user.serialize()).toStrictEqual({\n            __component: 'User',\n            id: 'user1',\n            email: '1@user.com',\n            fullName: 'User 1'\n          });\n\n          // ------\n\n          if (User.hasStore()) {\n            const store = User.getStore() as Store;\n\n            user = User.fork().instantiate({id: 'user1'});\n\n            store.startTrace();\n\n            expect(await user.load({})).toBe(user);\n            expect(store.getTrace()).toStrictEqual([\n              {\n                operation: 'load',\n                params: [\n                  user,\n                  {\n                    attributeSelector: {\n                      id: true\n                    },\n                    throwIfMissing: true\n                  }\n                ],\n                result: user\n              }\n            ]);\n\n            store.stopTrace();\n            store.startTrace();\n\n            expect(await user.load({})).toBe(user);\n            expect(store.getTrace()).toStrictEqual([]);\n\n            store.stopTrace();\n            store.startTrace();\n\n            expect(await user.load({id: true})).toBe(user);\n            expect(store.getTrace()).toStrictEqual([]);\n\n            store.stopTrace();\n            store.startTrace();\n\n            expect(await user.load({id: true, email: true})).toBe(user);\n            expect(store.getTrace()).toStrictEqual([\n              {\n                operation: 'load',\n                params: [\n                  user,\n                  {\n                    attributeSelector: {email: true},\n                    throwIfMissing: true\n                  }\n                ],\n                result: user\n              }\n            ]);\n\n            store.stopTrace();\n            store.startTrace();\n\n            expect(await user.load({id: true, email: true, fullName: true})).toBe(user);\n            expect(store.getTrace()).toStrictEqual([\n              {\n                operation: 'load',\n                params: [\n                  user,\n                  {\n                    attributeSelector: {fullName: true},\n                    throwIfMissing: true\n                  }\n                ],\n                result: user\n              }\n            ]);\n\n            store.stopTrace();\n            store.startTrace();\n\n            expect(await user.load({id: true, email: true, fullName: true})).toBe(user);\n            expect(store.getTrace()).toStrictEqual([]);\n\n            store.stopTrace();\n            store.startTrace();\n\n            expect(await user.load({fullName: true}, {reload: true})).toBe(user);\n            expect(store.getTrace()).toStrictEqual([\n              {\n                operation: 'load',\n                params: [\n                  user,\n                  {\n                    attributeSelector: {\n                      id: true,\n                      fullName: true\n                    },\n                    throwIfMissing: true\n                  }\n                ],\n                result: user\n              }\n            ]);\n\n            store.stopTrace();\n            store.startTrace();\n\n            expect(await user.load({organization: {}})).toBe(user);\n            expect(store.getTrace()).toStrictEqual([\n              {\n                operation: 'load',\n                params: [\n                  user,\n                  {\n                    attributeSelector: {organization: {id: true}},\n                    throwIfMissing: true\n                  }\n                ],\n                result: user\n              }\n            ]);\n\n            store.stopTrace();\n            store.startTrace();\n\n            expect(await user.load({organization: {}})).toBe(user);\n            expect(store.getTrace()).toStrictEqual([]);\n\n            store.stopTrace();\n          }\n\n          // ------\n\n          user = new (User.fork())({id: 'user1', email: '1@user.com', reference: 1});\n\n          await expect(user.load({})).rejects.toThrow(\n            \"Cannot load a storable component that is marked as new (component: 'User')\"\n          );\n\n          // --- With a referenced identifiable component loaded from a fork ---\n\n          UserFork = User.fork();\n          organization = await UserFork.Organization.get('org1');\n          const UserForkFork = UserFork.fork();\n          user = await UserForkFork.get('user1', {organization: {}});\n\n          expect(user.organization?.isForkOf(organization));\n\n          // ------\n\n          organization = User.fork().Organization.instantiate(\n            {slug: 'organization-1'},\n            {source: 'store'}\n          );\n\n          expect(await organization.load({})).toBe(organization);\n          expect(organization.serialize()).toStrictEqual({\n            __component: 'Organization',\n            id: 'org1',\n            slug: 'organization-1'\n          });\n\n          organization = User.fork().Organization.instantiate(\n            {slug: 'organization-1'},\n            {source: 'store'}\n          );\n\n          expect(await organization.load(true)).toBe(organization);\n          expect(organization.serialize()).toStrictEqual({\n            __component: 'Organization',\n            id: 'org1',\n            slug: 'organization-1',\n            name: 'Organization 1'\n          });\n\n          // ------\n\n          UserFork = User.fork();\n          user = UserFork.instantiate({id: 'user1'});\n          organization = UserFork.Organization.instantiate({id: 'org1'});\n\n          await Promise.all([\n            user.load({fullName: true, organization: {}}),\n            organization.load({name: true})\n          ]);\n\n          expect(user.serialize()).toStrictEqual({\n            __component: 'User',\n            id: 'user1',\n            fullName: 'User 1',\n            organization: {\n              __component: 'Organization',\n              id: 'org1'\n            }\n          });\n          expect(organization.serialize()).toStrictEqual({\n            __component: 'Organization',\n            id: 'org1',\n            name: 'Organization 1'\n          });\n        });\n\n        test('save()', async () => {\n          const User = await userClassProvider();\n\n          let UserFork = User.fork();\n          let PictureFork = UserFork.Picture;\n          let OrganizationFork = UserFork.Organization;\n\n          let user = new UserFork({\n            id: 'user2',\n            email: '2@user.com',\n            reference: 2,\n            fullName: 'User 2',\n            tags: ['newcomer'],\n            location: {country: 'USA', city: 'New York'},\n            picture: new PictureFork({type: 'JPEG', url: 'https://pictures.com/2-1.jpg'}),\n            organization: OrganizationFork.instantiate({id: 'org2'})\n          });\n\n          if (!(User.hasStore() || User.getRemoteComponent() !== undefined)) {\n            return await expect(user.save()).rejects.toThrow(\n              \"To be able to execute the save() method, a storable component should be registered in a store or have an exposed save() remote method (component: 'User')\"\n            );\n          }\n\n          expect(user.isNew()).toBe(true);\n          expect(user.picture!.isNew()).toBe(true);\n          expect(user.getAttribute('id').getValueSource()).toBe('local');\n          expect(user.getAttribute('fullName').getValueSource()).toBe('local');\n          expect(user.getAttribute('tags').getValueSource()).toBe('local');\n          expect(user.getAttribute('picture').getValueSource()).toBe('local');\n          expect(user.picture!.getAttribute('url').getValueSource()).toBe('local');\n\n          expect(await user.save()).toBe(user);\n\n          expect(user.isNew()).toBe(false);\n          // TODO: expect(user.picture!.isNew()).toBe(false);\n          expect(user.getAttribute('id').getValueSource()).toBe(\n            User.hasStore() ? 'store' : 'server'\n          );\n          expect(user.getAttribute('fullName').getValueSource()).toBe(\n            User.hasStore() ? 'store' : 'server'\n          );\n          expect(user.getAttribute('tags').getValueSource()).toBe(\n            User.hasStore() ? 'store' : 'server'\n          );\n          expect(user.getAttribute('picture').getValueSource()).toBe(\n            User.hasStore() ? 'store' : 'server'\n          );\n          expect(user.picture!.getAttribute('url').getValueSource()).toBe(\n            User.hasStore() ? 'store' : 'server'\n          );\n\n          UserFork = User.fork();\n          PictureFork = UserFork.Picture;\n          OrganizationFork = UserFork.Organization;\n\n          user = await UserFork.get('user2');\n\n          expect(user.serialize({includeReferencedComponents: true})).toStrictEqual({\n            __component: 'User',\n            id: 'user2',\n            email: '2@user.com',\n            reference: 2,\n            fullName: 'User 2',\n            accessLevel: 0,\n            tags: ['newcomer'],\n            location: {country: 'USA', city: 'New York'},\n            pastLocations: [],\n            picture: {\n              __component: 'Picture',\n              type: 'JPEG',\n              url: 'https://pictures.com/2-1.jpg'\n            },\n            pastPictures: [],\n            organization: {\n              __component: 'Organization',\n              id: 'org2',\n              slug: 'organization-2',\n              name: 'Organization 2'\n            },\n            emailIsVerified: false,\n            createdOn: {__date: CREATED_ON.toISOString()},\n            updatedOn: {__undefined: true}\n          });\n\n          // ------\n\n          user.fullName = 'User 2 (modified)';\n          user.accessLevel = 1;\n          user.tags = [];\n          user.location!.state = 'New York';\n          user.pastLocations.push({country: 'USA', city: 'San Francisco'});\n          user.picture!.url = 'https://pictures.com/2-2.jpg';\n          user.pastPictures.push(\n            new PictureFork({type: 'JPEG', url: 'https://pictures.com/2-1.jpg'})\n          );\n          user.organization = OrganizationFork.instantiate({id: 'org1'});\n          user.updatedOn = UPDATED_ON;\n\n          expect(await user.save()).toBe(user);\n\n          user = await User.fork().get('user2', {\n            fullName: true,\n            accessLevel: true,\n            tags: true,\n            location: true,\n            pastLocations: true,\n            picture: true,\n            pastPictures: true,\n            organization: true,\n            updatedOn: true\n          });\n\n          expect(user.serialize({includeReferencedComponents: true})).toStrictEqual({\n            __component: 'User',\n            id: 'user2',\n            fullName: 'User 2 (modified)',\n            accessLevel: 1,\n            tags: [],\n            location: {country: 'USA', state: 'New York', city: 'New York'},\n            pastLocations: [{country: 'USA', city: 'San Francisco'}],\n            picture: {\n              __component: 'Picture',\n              type: 'JPEG',\n              url: 'https://pictures.com/2-2.jpg'\n            },\n            pastPictures: [\n              {\n                __component: 'Picture',\n                type: 'JPEG',\n                url: 'https://pictures.com/2-1.jpg'\n              }\n            ],\n            organization: {\n              __component: 'Organization',\n              id: 'org1',\n              slug: 'organization-1',\n              name: 'Organization 1'\n            },\n            updatedOn: {__date: UPDATED_ON.toISOString()}\n          });\n\n          // ------\n\n          user.location = {country: 'USA'};\n          delete user.location.state;\n          delete user.location.city;\n          delete user.pastLocations[0].city;\n          user.picture!.type = undefined;\n          user.pastPictures[0].type = undefined;\n          user.organization = undefined;\n          user.updatedOn = undefined;\n\n          expect(await user.save()).toBe(user);\n\n          user = await User.fork().get('user2', {\n            location: true,\n            pastLocations: true,\n            picture: true,\n            pastPictures: true,\n            organization: true,\n            updatedOn: true\n          });\n\n          expect(user.serialize()).toStrictEqual({\n            __component: 'User',\n            id: 'user2',\n            location: {country: 'USA'},\n            pastLocations: [{country: 'USA'}],\n            picture: {\n              __component: 'Picture',\n              type: {__undefined: true},\n              url: 'https://pictures.com/2-2.jpg'\n            },\n            pastPictures: [\n              {\n                __component: 'Picture',\n                type: {__undefined: true},\n                url: 'https://pictures.com/2-1.jpg'\n              }\n            ],\n            organization: {__undefined: true},\n            updatedOn: {__undefined: true}\n          });\n\n          // ------\n\n          // Undefined values in object attributes should not be saved\n          user.location!.country = undefined;\n          user.pastLocations[0].country = undefined;\n\n          expect(await user.save()).toBe(user);\n\n          user = await User.fork().get('user2', {location: true, pastLocations: true});\n\n          expect(user.serialize()).toStrictEqual({\n            __component: 'User',\n            id: 'user2',\n            location: {},\n            pastLocations: [{}]\n          });\n\n          // ------\n\n          if (User.hasStore()) {\n            const store = User.getStore() as Store;\n\n            user = await User.fork().get('user2', {fullName: true, accessLevel: true});\n\n            expect(user.serialize()).toStrictEqual({\n              __component: 'User',\n              id: 'user2',\n              fullName: 'User 2 (modified)',\n              accessLevel: 1\n            });\n\n            user.accessLevel = 2;\n\n            store.startTrace();\n\n            expect(await user.save()).toBe(user);\n\n            expect(store.getTrace()).toStrictEqual([\n              {\n                operation: 'save',\n                params: [\n                  user,\n                  {\n                    attributeSelector: {id: true, accessLevel: true},\n                    throwIfMissing: true,\n                    throwIfExists: false\n                  }\n                ],\n                result: user\n              }\n            ]);\n\n            store.stopTrace();\n\n            user = await User.fork().get('user2', {fullName: true, accessLevel: true});\n\n            expect(user.serialize()).toStrictEqual({\n              __component: 'User',\n              id: 'user2',\n              fullName: 'User 2 (modified)',\n              accessLevel: 2\n            });\n\n            store.startTrace();\n\n            expect(await user.save()).toBe(user);\n\n            expect(store.getTrace()).toStrictEqual([]);\n\n            store.stopTrace();\n          }\n\n          // ------\n\n          user = await User.fork().get('user2', {pastPictures: {type: true}});\n\n          expect(user.serialize()).toStrictEqual({\n            __component: 'User',\n            id: 'user2',\n            pastPictures: [{__component: 'Picture', type: {__undefined: true}}]\n          });\n\n          user.pastPictures[0].type = 'JPEG';\n\n          await expect(user.save()).rejects.toThrow(\n            \"Cannot save an array item that has some unset attributes (component: 'User.Picture')\"\n          );\n\n          await expect(user.save({pastPictures: {type: true}})).rejects.toThrow(\n            \"Cannot save an array item that has some unset attributes (component: 'User.Picture')\"\n          );\n\n          // ------\n\n          user = User.fork().instantiate({id: 'user3'});\n          user.fullName = 'User 3';\n\n          expect(await user.save(true, {throwIfMissing: false})).toBe(undefined);\n          await expect(user.save()).rejects.toThrow(\n            \"Cannot save a non-new component that is missing from the store (component: 'User', id: 'user3')\"\n          );\n\n          // ------\n\n          user = new (User.fork())({\n            id: 'user1',\n            email: '1@user.com',\n            reference: 1,\n            fullName: 'User 1 (modified)'\n          });\n\n          expect(await user.save(true, {throwIfExists: false})).toBe(undefined);\n          await expect(user.save()).rejects.toThrow(\n            \"Cannot save a new component that already exists in the store (component: 'User', id: 'user1')\"\n          );\n\n          // ------\n\n          user = User.fork().instantiate({id: 'user3'});\n          user.fullName = 'User 3';\n\n          await expect(\n            user.save(true, {throwIfMissing: true, throwIfExists: true})\n          ).rejects.toThrow(\n            \"The 'throwIfMissing' and 'throwIfExists' options cannot be both set to true\"\n          );\n\n          // ------\n\n          if (User.hasStore() && User.getStore() instanceof MongoDBStore) {\n            user = new (User.fork())({\n              id: 'user3',\n              email: '1@user.com',\n              reference: 3\n            });\n\n            await expect(user.save()).rejects.toThrow(\n              \"Cannot save a component with an attribute value that should be unique but already exists in the store (component: 'User', id: 'user3', index: 'email [unique]')\"\n            );\n\n            user = User.fork().instantiate({id: 'user2'});\n            user.reference = 1;\n\n            await expect(user.save()).rejects.toThrow(\n              \"Cannot save a component with an attribute value that should be unique but already exists in the store (component: 'User', id: 'user2', index: 'reference [unique]')\"\n            );\n          }\n        });\n\n        test('delete()', async () => {\n          const User = await userClassProvider();\n\n          let user = User.fork().instantiate({id: 'user1'});\n\n          if (!(User.hasStore() || User.getRemoteComponent() !== undefined)) {\n            return await expect(user.delete()).rejects.toThrow(\n              \"To be able to execute the delete() method, a storable component should be registered in a store or have an exposed delete() remote method (component: 'User')\"\n            );\n          }\n\n          expect(user.getIsDeletedMark()).toBe(false);\n          expect(await user.delete()).toBe(user);\n          expect(user.getIsDeletedMark()).toBe(true);\n\n          await expect(user.delete()).rejects.toThrow(\n            \"Cannot delete a component that is missing from the store (component: 'User', id: 'user1'\"\n          );\n\n          expect(await user.delete({throwIfMissing: false})).toBeUndefined();\n\n          user = new (User.fork())({id: 'user1', email: '1@user.com', reference: 1});\n\n          await expect(user.delete()).rejects.toThrow(\n            \"Cannot delete a storable component that is new (component: 'User')\"\n          );\n        });\n\n        test('find()', async () => {\n          const User = await userClassProvider();\n\n          if (!(User.hasStore() || User.getRemoteComponent() !== undefined)) {\n            return await expect(User.fork().find()).rejects.toThrow(\n              \"To be able to execute the find() method, a storable component should be registered in a store or have an exposed find() remote method (component: 'User')\"\n            );\n          }\n\n          // === Simple queries ===\n\n          // --- Without a query ---\n\n          let users = await User.fork().find();\n\n          expect(serialize(users, {includeReferencedComponents: true})).toStrictEqual([\n            {\n              __component: 'User',\n              id: 'user1',\n              email: '1@user.com',\n              reference: 1,\n              fullName: 'User 1',\n              accessLevel: 0,\n              tags: ['spammer', 'blocked'],\n              location: {country: 'USA', city: 'Paris'},\n              pastLocations: [\n                {country: 'USA', city: 'Nice'},\n                {country: 'USA', city: 'New York'}\n              ],\n              picture: {\n                __component: 'Picture',\n                type: 'JPEG',\n                url: 'https://pictures.com/1-2.jpg'\n              },\n              pastPictures: [\n                {\n                  __component: 'Picture',\n                  type: 'JPEG',\n                  url: 'https://pictures.com/1-1.jpg'\n                },\n                {\n                  __component: 'Picture',\n                  type: 'PNG',\n                  url: 'https://pictures.com/1-1.png'\n                }\n              ],\n              organization: {\n                __component: 'Organization',\n                id: 'org1',\n                slug: 'organization-1',\n                name: 'Organization 1'\n              },\n              emailIsVerified: false,\n              createdOn: {__date: CREATED_ON.toISOString()},\n              updatedOn: {__undefined: true}\n            },\n            {\n              __component: 'User',\n              id: 'user11',\n              email: '11@user.com',\n              reference: 11,\n              fullName: 'User 11',\n              accessLevel: 3,\n              tags: ['owner', 'admin'],\n              location: {country: 'USA'},\n              pastLocations: [{country: 'France'}],\n              picture: {\n                __component: 'Picture',\n                type: 'JPEG',\n                url: 'https://pictures.com/11-1.jpg'\n              },\n              pastPictures: [\n                {\n                  __component: 'Picture',\n                  type: 'PNG',\n                  url: 'https://pictures.com/11-1.png'\n                }\n              ],\n              organization: {\n                __component: 'Organization',\n                id: 'org2',\n                slug: 'organization-2',\n                name: 'Organization 2'\n              },\n              emailIsVerified: true,\n              createdOn: {__date: CREATED_ON.toISOString()},\n              updatedOn: {__undefined: true}\n            },\n            {\n              __component: 'User',\n              id: 'user12',\n              email: '12@user.com',\n              reference: 12,\n              fullName: 'User 12',\n              accessLevel: 1,\n              tags: [],\n              location: {country: 'France', city: 'Paris'},\n              pastLocations: [\n                {country: 'France', city: 'Nice'},\n                {country: 'Japan', city: 'Tokyo'}\n              ],\n              picture: {\n                __component: 'Picture',\n                type: 'PNG',\n                url: 'https://pictures.com/12-3.png'\n              },\n              pastPictures: [\n                {\n                  __component: 'Picture',\n                  type: 'PNG',\n                  url: 'https://pictures.com/12-2.png'\n                },\n                {\n                  __component: 'Picture',\n                  type: 'PNG',\n                  url: 'https://pictures.com/12-1.png'\n                }\n              ],\n              organization: {\n                __component: 'Organization',\n                id: 'org2'\n              },\n              emailIsVerified: true,\n              createdOn: {__date: CREATED_ON.toISOString()},\n              updatedOn: {__undefined: true}\n            },\n            {\n              __component: 'User',\n              id: 'user13',\n              email: '13@user.com',\n              reference: 13,\n              fullName: 'User 13',\n              accessLevel: 3,\n              tags: ['admin'],\n              location: {__undefined: true},\n              pastLocations: [],\n              picture: {__undefined: true},\n              pastPictures: [],\n              organization: {__undefined: true},\n              emailIsVerified: false,\n              createdOn: {__date: CREATED_ON.toISOString()},\n              updatedOn: {__undefined: true}\n            }\n          ]);\n\n          // --- With a simple query ---\n\n          users = await User.fork().find({fullName: 'User 12'});\n\n          expect(serialize(users, {includeReferencedComponents: true})).toStrictEqual([\n            {\n              __component: 'User',\n              id: 'user12',\n              email: '12@user.com',\n              reference: 12,\n              fullName: 'User 12',\n              accessLevel: 1,\n              tags: [],\n              location: {country: 'France', city: 'Paris'},\n              pastLocations: [\n                {country: 'France', city: 'Nice'},\n                {country: 'Japan', city: 'Tokyo'}\n              ],\n              picture: {\n                __component: 'Picture',\n                type: 'PNG',\n                url: 'https://pictures.com/12-3.png'\n              },\n              pastPictures: [\n                {\n                  __component: 'Picture',\n                  type: 'PNG',\n                  url: 'https://pictures.com/12-2.png'\n                },\n                {\n                  __component: 'Picture',\n                  type: 'PNG',\n                  url: 'https://pictures.com/12-1.png'\n                }\n              ],\n              organization: {\n                __component: 'Organization',\n                id: 'org2',\n                slug: 'organization-2',\n                name: 'Organization 2'\n              },\n              emailIsVerified: true,\n              createdOn: {__date: CREATED_ON.toISOString()},\n              updatedOn: {__undefined: true}\n            }\n          ]);\n\n          // --- With an attribute selector ---\n\n          users = await User.fork().find({accessLevel: 3}, {email: true});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user11', email: '11@user.com'},\n            {__component: 'User', id: 'user13', email: '13@user.com'}\n          ]);\n\n          users = await User.fork().find({emailIsVerified: false}, {email: true});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user1', email: '1@user.com'},\n            {__component: 'User', id: 'user13', email: '13@user.com'}\n          ]);\n\n          // --- With a query involving two attributes ---\n\n          users = await User.fork().find({accessLevel: 3, emailIsVerified: true}, {});\n\n          expect(serialize(users)).toStrictEqual([{__component: 'User', id: 'user11'}]);\n\n          // --- With 'sort' ---\n\n          users = await User.fork().find({}, {accessLevel: true}, {sort: {accessLevel: 'asc'}});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user1', accessLevel: 0},\n            {__component: 'User', id: 'user12', accessLevel: 1},\n            {__component: 'User', id: 'user11', accessLevel: 3},\n            {__component: 'User', id: 'user13', accessLevel: 3}\n          ]);\n\n          users = await User.fork().find({}, {accessLevel: true}, {sort: {accessLevel: 'desc'}});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user11', accessLevel: 3},\n            {__component: 'User', id: 'user13', accessLevel: 3},\n            {__component: 'User', id: 'user12', accessLevel: 1},\n            {__component: 'User', id: 'user1', accessLevel: 0}\n          ]);\n\n          users = await User.fork().find(\n            {},\n            {reference: true, accessLevel: true},\n            {sort: {accessLevel: 'asc', reference: 'desc'}}\n          );\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user1', reference: 1, accessLevel: 0},\n            {__component: 'User', id: 'user12', reference: 12, accessLevel: 1},\n            {__component: 'User', id: 'user13', reference: 13, accessLevel: 3},\n            {__component: 'User', id: 'user11', reference: 11, accessLevel: 3}\n          ]);\n\n          // --- With 'skip' ---\n\n          users = await User.fork().find({}, {}, {skip: 2});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user12'},\n            {__component: 'User', id: 'user13'}\n          ]);\n\n          // --- With 'limit' ---\n\n          users = await User.fork().find({}, {}, {limit: 2});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user1'},\n            {__component: 'User', id: 'user11'}\n          ]);\n\n          // --- With 'skip' and 'limit' ---\n\n          users = await User.fork().find({}, {}, {skip: 1, limit: 2});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user11'},\n            {__component: 'User', id: 'user12'}\n          ]);\n\n          // --- With 'sort', 'skip', and 'limit' ---\n\n          users = await User.fork().find({}, {}, {sort: {id: 'desc'}, skip: 1, limit: 2});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user12'},\n            {__component: 'User', id: 'user11'}\n          ]);\n\n          await expect(User.fork().find({unknownAttribute: 1})).rejects.toThrow(\n            \"An unknown attribute was specified in a query (component: 'User', attribute: 'unknownAttribute')\"\n          );\n\n          // === Advanced queries ===\n\n          // --- With a basic operator ---\n\n          // - '$equal' -\n\n          users = await User.fork().find({accessLevel: {$equal: 0}}, {});\n\n          expect(serialize(users)).toStrictEqual([{__component: 'User', id: 'user1'}]);\n\n          await expect(User.fork().find({accessLevel: {$equal: /0/}}, {})).rejects.toThrow(\n            \"Expected a scalar value of the operator '$equal', but received a value of type 'RegExp' (query: '{\\\"accessLevel\\\":{\\\"$equal\\\":{}}}')\"\n          );\n\n          expect(serialize(users)).toStrictEqual([{__component: 'User', id: 'user1'}]);\n\n          // - '$notEqual' -\n\n          users = await User.fork().find({accessLevel: {$notEqual: 3}}, {});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user1'},\n            {__component: 'User', id: 'user12'}\n          ]);\n\n          // - '$greaterThan' -\n\n          users = await User.fork().find({accessLevel: {$greaterThan: 3}}, {});\n\n          expect(serialize(users)).toStrictEqual([]);\n\n          users = await User.fork().find({accessLevel: {$greaterThan: 2}}, {});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user11'},\n            {__component: 'User', id: 'user13'}\n          ]);\n\n          // - '$greaterThanOrEqual' -\n\n          users = await User.fork().find({accessLevel: {$greaterThanOrEqual: 3}}, {});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user11'},\n            {__component: 'User', id: 'user13'}\n          ]);\n\n          // - '$lessThan' -\n\n          users = await User.fork().find({accessLevel: {$lessThan: 1}}, {});\n\n          expect(serialize(users)).toStrictEqual([{__component: 'User', id: 'user1'}]);\n\n          // - '$lessThanOrEqual' -\n\n          users = await User.fork().find({accessLevel: {$lessThanOrEqual: 1}}, {});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user1'},\n            {__component: 'User', id: 'user12'}\n          ]);\n\n          // - '$in' -\n\n          users = await User.fork().find({accessLevel: {$in: []}}, {});\n\n          expect(serialize(users)).toStrictEqual([]);\n\n          users = await User.fork().find({accessLevel: {$in: [2, 4, 5]}}, {});\n\n          expect(serialize(users)).toStrictEqual([]);\n\n          users = await User.fork().find({accessLevel: {$in: [0, 1]}}, {});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user1'},\n            {__component: 'User', id: 'user12'}\n          ]);\n\n          // --- With two basic operators ---\n\n          users = await User.fork().find({accessLevel: {$greaterThan: 1, $lessThan: 3}}, {});\n\n          expect(serialize(users)).toStrictEqual([]);\n\n          users = await User.fork().find({accessLevel: {$greaterThanOrEqual: 0, $lessThan: 2}}, {});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user1'},\n            {__component: 'User', id: 'user12'}\n          ]);\n\n          // --- With an impossible expression ---\n\n          users = await User.fork().find({accessLevel: {$greaterThan: 1, $equal: 1}}, {});\n\n          expect(serialize(users)).toStrictEqual([]);\n\n          // --- With a string operator ---\n\n          // - '$includes' -\n\n          users = await User.fork().find({email: {$includes: '.org'}}, {});\n\n          expect(serialize(users)).toStrictEqual([]);\n\n          users = await User.fork().find({email: {$includes: '2'}}, {});\n\n          expect(serialize(users)).toStrictEqual([{__component: 'User', id: 'user12'}]);\n\n          await expect(User.fork().find({email: {$includes: 2}}, {})).rejects.toThrow(\n            \"Expected a string as value of the operator '$includes', but received a value of type 'number' (query: '{\\\"email\\\":{\\\"$includes\\\":2}}')\"\n          );\n\n          // - '$startsWith' -\n\n          users = await User.fork().find({email: {$startsWith: '2'}}, {});\n\n          expect(serialize(users)).toStrictEqual([]);\n\n          users = await User.fork().find({email: {$startsWith: '1@'}}, {});\n\n          expect(serialize(users)).toStrictEqual([{__component: 'User', id: 'user1'}]);\n\n          // - '$endsWith' -\n\n          users = await User.fork().find({location: {city: {$endsWith: 'town'}}}, {});\n\n          expect(serialize(users)).toStrictEqual([]);\n\n          users = await User.fork().find({location: {city: {$endsWith: 'ris'}}}, {});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user1'},\n            {__component: 'User', id: 'user12'}\n          ]);\n\n          // - '$matches' -\n\n          users = await User.fork().find({location: {country: {$matches: /usa/}}}, {});\n\n          expect(serialize(users)).toStrictEqual([]);\n\n          users = await User.fork().find({location: {country: {$matches: /usa/i}}}, {});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user1'},\n            {__component: 'User', id: 'user11'}\n          ]);\n\n          await expect(User.fork().find({location: {country: {$matches: 'usa'}}})).rejects.toThrow(\n            'Expected a regular expression as value of the operator \\'$matches\\', but received a value of type \\'string\\' (query: \\'{\"country\":{\"$matches\":\"usa\"}}\\')'\n          );\n\n          // --- With a logical operator ---\n\n          // - '$not' -\n\n          users = await User.fork().find({createdOn: {$not: CREATED_ON}}, {});\n\n          expect(serialize(users)).toStrictEqual([]);\n\n          users = await User.fork().find({accessLevel: {$not: {$lessThan: 3}}}, {});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user11'},\n            {__component: 'User', id: 'user13'}\n          ]);\n\n          if (User.hasStore() && User.getStore() instanceof MongoDBStore) {\n            // TODO: Make the following test passes with a MemoryStore\n\n            users = await User.fork().find({tags: {$not: {$in: ['admin']}}}, {});\n\n            expect(serialize(users)).toStrictEqual([\n              {__component: 'User', id: 'user1'},\n              {__component: 'User', id: 'user12'}\n            ]);\n          }\n\n          // - '$and' -\n\n          users = await User.fork().find({$and: [{tags: 'owner'}, {emailIsVerified: false}]}, {});\n\n          expect(serialize(users)).toStrictEqual([]);\n\n          users = await User.fork().find({$and: [{tags: 'admin'}, {emailIsVerified: true}]}, {});\n\n          expect(serialize(users)).toStrictEqual([{__component: 'User', id: 'user11'}]);\n\n          // - '$or' -\n\n          users = await User.fork().find(\n            {$or: [{accessLevel: {$lessThan: 0}}, {accessLevel: {$greaterThan: 3}}]},\n            {}\n          );\n\n          expect(serialize(users)).toStrictEqual([]);\n\n          users = await User.fork().find({$or: [{accessLevel: 0}, {accessLevel: 1}]}, {});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user1'},\n            {__component: 'User', id: 'user12'}\n          ]);\n\n          // - '$nor' -\n\n          users = await User.fork().find(\n            {$nor: [{emailIsVerified: false}, {emailIsVerified: true}]},\n            {}\n          );\n\n          expect(serialize(users)).toStrictEqual([]);\n\n          users = await User.fork().find({$nor: [{accessLevel: 0}, {accessLevel: 1}]}, {});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user11'},\n            {__component: 'User', id: 'user13'}\n          ]);\n\n          // --- With an object attribute ---\n\n          users = await User.fork().find({location: {country: 'Japan'}}, {});\n\n          expect(serialize(users)).toStrictEqual([]);\n\n          users = await User.fork().find({location: {country: 'France'}}, {});\n\n          expect(serialize(users)).toStrictEqual([{__component: 'User', id: 'user12'}]);\n\n          users = await User.fork().find({location: {country: 'USA'}}, {});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user1'},\n            {__component: 'User', id: 'user11'}\n          ]);\n\n          users = await User.fork().find({location: {country: 'USA', city: 'Paris'}}, {});\n\n          expect(serialize(users)).toStrictEqual([{__component: 'User', id: 'user1'}]);\n\n          users = await User.fork().find({location: undefined}, {});\n\n          expect(serialize(users)).toStrictEqual([{__component: 'User', id: 'user13'}]);\n\n          // --- With an array attribute ---\n\n          // - '$some' -\n\n          users = await User.fork().find({tags: {$some: 'angel'}}, {});\n\n          expect(serialize(users)).toStrictEqual([]);\n\n          users = await User.fork().find({tags: {$some: 'blocked'}}, {});\n\n          expect(serialize(users)).toStrictEqual([{__component: 'User', id: 'user1'}]);\n\n          users = await User.fork().find({tags: {$some: 'admin'}}, {});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user11'},\n            {__component: 'User', id: 'user13'}\n          ]);\n\n          // '$some' should be implicit\n          users = await User.fork().find({tags: 'admin'}, {});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user11'},\n            {__component: 'User', id: 'user13'}\n          ]);\n\n          // '$includes' should be replaced by '$some' when '$some' is missing\n          users = await User.fork().find({tags: {$includes: 'admin'}}, {});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user11'},\n            {__component: 'User', id: 'user13'}\n          ]);\n\n          // When '$some' is present, '$includes' remains a string operator\n          users = await User.fork().find({tags: {$some: {$includes: 'lock'}}}, {});\n\n          expect(serialize(users)).toStrictEqual([{__component: 'User', id: 'user1'}]);\n\n          // - '$every' -\n\n          users = await User.fork().find({tags: {$every: 'admin'}}, {});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user12'},\n            {__component: 'User', id: 'user13'}\n          ]);\n\n          // - '$length' -\n\n          users = await User.fork().find({tags: {$length: 3}}, {});\n\n          expect(serialize(users)).toStrictEqual([]);\n\n          users = await User.fork().find({tags: {$length: 0}}, {});\n\n          expect(serialize(users)).toStrictEqual([{__component: 'User', id: 'user12'}]);\n\n          users = await User.fork().find({tags: {$length: 2}}, {});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user1'},\n            {__component: 'User', id: 'user11'}\n          ]);\n\n          // --- With an array of object attribute ---\n\n          users = await User.fork().find({pastLocations: {country: 'Canada'}}, {});\n\n          expect(serialize(users)).toStrictEqual([]);\n\n          users = await User.fork().find({pastLocations: {country: 'Japan'}}, {});\n\n          expect(serialize(users)).toStrictEqual([{__component: 'User', id: 'user12'}]);\n\n          users = await User.fork().find({pastLocations: {country: 'France'}}, {});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user11'},\n            {__component: 'User', id: 'user12'}\n          ]);\n\n          users = await User.fork().find({pastLocations: {country: 'USA', city: 'Nice'}}, {});\n\n          expect(serialize(users)).toStrictEqual([{__component: 'User', id: 'user1'}]);\n\n          users = await User.fork().find({pastLocations: {city: undefined}}, {});\n\n          expect(serialize(users)).toStrictEqual([{__component: 'User', id: 'user11'}]);\n\n          // --- With an array of embedded components ---\n\n          // - '$some' -\n\n          users = await User.fork().find({pastPictures: {type: 'JPEG'}}, {});\n\n          expect(serialize(users)).toStrictEqual([{__component: 'User', id: 'user1'}]);\n\n          users = await User.fork().find({pastPictures: {type: 'PNG'}}, {});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user1'},\n            {__component: 'User', id: 'user11'},\n            {__component: 'User', id: 'user12'}\n          ]);\n\n          // - '$length' -\n\n          users = await User.fork().find({pastPictures: {$length: 0}}, {});\n\n          expect(serialize(users)).toStrictEqual([{__component: 'User', id: 'user13'}]);\n\n          users = await User.fork().find({pastPictures: {$length: 1}}, {});\n\n          expect(serialize(users)).toStrictEqual([{__component: 'User', id: 'user11'}]);\n\n          users = await User.fork().find({pastPictures: {$length: 2}}, {});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user1'},\n            {__component: 'User', id: 'user12'}\n          ]);\n\n          users = await User.fork().find({pastPictures: {$length: 3}}, {});\n\n          expect(serialize(users)).toStrictEqual([]);\n\n          // --- With a component specified as query ---\n\n          let UserFork = User.fork();\n\n          const user = await UserFork.get('user1', {email: true});\n\n          users = await UserFork.find(user, {fullName: true});\n\n          expect(users).toHaveLength(1);\n          expect(users[0]).toBe(user);\n          expect(serialize(user)).toStrictEqual({\n            __component: 'User',\n            id: 'user1',\n            email: '1@user.com',\n            fullName: 'User 1'\n          });\n\n          // --- With an a referenced component specified in a query ---\n\n          UserFork = User.fork();\n\n          const organization = await UserFork.Organization.get('org2');\n\n          users = await UserFork.find({organization}, {});\n\n          expect(serialize(users)).toStrictEqual([\n            {\n              __component: 'User',\n              id: 'user11'\n            },\n            {\n              __component: 'User',\n              id: 'user12'\n            }\n          ]);\n\n          // --- With an array of referenced components specified in a query (using '$in') ---\n\n          UserFork = User.fork();\n\n          const organization1 = UserFork.Organization.instantiate({id: 'org1'});\n          const organization2 = UserFork.Organization.instantiate({id: 'org2'});\n\n          users = await UserFork.find({organization: {$in: [organization1, organization2]}}, {});\n\n          expect(serialize(users)).toStrictEqual([\n            {__component: 'User', id: 'user1'},\n            {__component: 'User', id: 'user11'},\n            {__component: 'User', id: 'user12'}\n          ]);\n        });\n\n        test('count()', async () => {\n          const User = await userClassProvider();\n\n          if (!(User.hasStore() || User.getRemoteComponent() !== undefined)) {\n            return await expect(User.fork().count()).rejects.toThrow(\n              \"To be able to execute the count() method, a storable component should be registered in a store or have an exposed count() remote method (component: 'User')\"\n            );\n          }\n\n          // === Simple queries ===\n\n          expect(await User.fork().count()).toBe(4);\n\n          expect(await User.fork().count({fullName: 'User 12'})).toBe(1);\n\n          expect(await User.fork().count({accessLevel: 3})).toBe(2);\n\n          expect(await User.fork().count({emailIsVerified: false})).toBe(2);\n\n          expect(await User.fork().count({accessLevel: 3, emailIsVerified: true})).toBe(1);\n\n          // === Advanced queries ===\n\n          // --- With an array attribute ---\n\n          expect(await User.fork().count({tags: {$some: 'angel'}})).toBe(0);\n\n          expect(await User.fork().count({tags: {$some: 'blocked'}})).toBe(1);\n\n          expect(await User.fork().count({tags: {$some: 'admin'}})).toBe(2);\n\n          expect(await User.fork().count({tags: 'admin'})).toBe(2);\n\n          // --- With a component specified as query ---\n\n          const UserFork = User.fork();\n\n          const user = UserFork.instantiate({reference: 1});\n\n          expect(await UserFork.count(user)).toBe(1);\n        });\n      });\n    }\n  });\n\n  describe('Loaders', () => {\n    test('getStorableAttributesWithLoader()', async () => {\n      const User = getUserClass();\n\n      const attributes = Array.from(User.prototype.getStorableAttributesWithLoader());\n\n      expect(attributes).toHaveLength(1);\n      expect(attributes[0].getName()).toBe('firstName');\n    });\n\n    test('getStorableComputedAttributes()', async () => {\n      const User = getUserClass();\n\n      const attributes = Array.from(User.prototype.getStorableComputedAttributes());\n\n      expect(attributes).toHaveLength(1);\n      expect(attributes[0].getName()).toBe('firstName');\n    });\n\n    test('load()', async () => {\n      const User = getUserClass();\n\n      let user = User.fork().instantiate({id: 'user1'});\n      await user.load({});\n\n      expect(user.getAttribute('firstName').isSet()).toBe(false);\n\n      user = User.fork().instantiate({id: 'user1'});\n      await user.load({firstName: true});\n\n      expect(user.firstName).toBe('User');\n\n      user = User.fork().instantiate({id: 'user1'});\n      await user.load({fullName: true, firstName: true});\n\n      expect(user.serialize()).toStrictEqual({\n        __component: 'User',\n        id: 'user1',\n        fullName: 'User 1',\n        firstName: 'User'\n      });\n    });\n\n    function getUserClass() {\n      class Picture extends BasePicture {}\n\n      class Organization extends BaseOrganization {}\n\n      class User extends BaseUser {\n        @provide() static Picture = Picture;\n        @provide() static Organization = Organization;\n\n        @loader(async function (this: User) {\n          await this.load({fullName: true});\n\n          const firstName = this.fullName.split(' ')[0];\n\n          return firstName;\n        })\n        @attribute('string')\n        firstName!: string;\n      }\n\n      const store = new MemoryStore({initialCollections: getInitialCollections()});\n\n      store.registerRootComponent(User);\n\n      return User;\n    }\n  });\n\n  describe('Finders', () => {\n    test('getStorablePropertiesWithFinder()', async () => {\n      const User = getUserClass();\n\n      const properties = Array.from(User.prototype.getStorablePropertiesWithFinder());\n\n      expect(properties).toHaveLength(2);\n      expect(properties[0].getName()).toBe('hasNoAccess');\n      expect(properties[1].getName()).toBe('hasAccessLevel');\n    });\n\n    test('getStorableComputedAttributes()', async () => {\n      const User = getUserClass();\n\n      const attributes = Array.from(User.prototype.getStorableComputedAttributes());\n\n      expect(attributes).toHaveLength(1);\n      expect(attributes[0].getName()).toBe('hasNoAccess');\n    });\n\n    test('find()', async () => {\n      const User = getUserClass();\n\n      let users = await User.fork().find({hasNoAccess: true}, {});\n\n      expect(serialize(users)).toStrictEqual([{__component: 'User', id: 'user1'}]);\n\n      users = await User.fork().find({hasNoAccess: true}, {hasNoAccess: true});\n\n      expect(serialize(users)).toStrictEqual([\n        {__component: 'User', id: 'user1', accessLevel: 0, hasNoAccess: true}\n      ]);\n\n      users = await User.fork().find({hasAccessLevel: 3}, {});\n\n      expect(serialize(users)).toStrictEqual([\n        {__component: 'User', id: 'user11'},\n        {__component: 'User', id: 'user13'}\n      ]);\n    });\n\n    function getUserClass() {\n      class Picture extends BasePicture {}\n\n      class Organization extends BaseOrganization {}\n\n      class User extends BaseUser {\n        @provide() static Picture = Picture;\n        @provide() static Organization = Organization;\n\n        @loader(async function (this: User) {\n          await this.load({accessLevel: true});\n\n          return this.accessLevel === 0;\n        })\n        @finder(async function () {\n          return {accessLevel: 0};\n        })\n        @attribute('boolean?')\n        hasNoAccess?: string;\n\n        @finder(async function (accessLevel) {\n          return {accessLevel};\n        })\n        @method()\n        async hasAccessLevel(accessLevel: number) {\n          await this.load({accessLevel: true});\n\n          return this.accessLevel === accessLevel;\n        }\n      }\n\n      const store = new MemoryStore({initialCollections: getInitialCollections()});\n\n      store.registerRootComponent(User);\n\n      return User;\n    }\n  });\n\n  describe('Hooks', () => {\n    test('getStorableAttributesWithHook()', async () => {\n      const User = getUserClass();\n\n      const getAttributesWithHook = (\n        storable: StorableComponent,\n        name: StorableAttributeHookName,\n        {\n          attributeSelector = true,\n          setAttributesOnly = false\n        }: {attributeSelector?: AttributeSelector; setAttributesOnly?: boolean} = {}\n      ) =>\n        Array.from(\n          storable.getStorableAttributesWithHook(name, {attributeSelector, setAttributesOnly})\n        ).map((attribute) => attribute.getName());\n\n      expect(getAttributesWithHook(User.prototype, 'beforeLoad')).toEqual(['email', 'fullName']);\n\n      const user = User.fork().instantiate({id: 'user1'});\n\n      expect(getAttributesWithHook(user, 'beforeLoad', {setAttributesOnly: true})).toEqual([]);\n\n      await user.load();\n\n      expect(getAttributesWithHook(user, 'beforeLoad', {setAttributesOnly: true})).toEqual([\n        'email',\n        'fullName'\n      ]);\n\n      expect(\n        getAttributesWithHook(user, 'beforeLoad', {\n          attributeSelector: {fullName: true},\n          setAttributesOnly: true\n        })\n      ).toEqual(['fullName']);\n\n      expect(\n        getAttributesWithHook(user, 'beforeLoad', {attributeSelector: {}, setAttributesOnly: true})\n      ).toEqual([]);\n    });\n\n    test('beforeLoad() and afterLoad()', async () => {\n      const User = getUserClass();\n\n      const user = User.fork().instantiate({id: 'user1'});\n\n      expect(hookTracker.get(user, 'beforeLoadHasBeenCalled')).toBeUndefined();\n      expect(hookTracker.get(user, 'afterLoadHasBeenCalled')).toBeUndefined();\n      expect(\n        hookTracker.get(user.getAttribute('email'), 'beforeLoadHasBeenCalled')\n      ).toBeUndefined();\n      expect(hookTracker.get(user.getAttribute('email'), 'afterLoadHasBeenCalled')).toBeUndefined();\n      expect(\n        hookTracker.get(user.getAttribute('fullName'), 'beforeLoadHasBeenCalled')\n      ).toBeUndefined();\n      expect(\n        hookTracker.get(user.getAttribute('fullName'), 'afterLoadHasBeenCalled')\n      ).toBeUndefined();\n\n      await user.load();\n\n      expect(hookTracker.get(user, 'beforeLoadHasBeenCalled')).toBe(true);\n      expect(hookTracker.get(user, 'afterLoadHasBeenCalled')).toBe(true);\n      expect(hookTracker.get(user.getAttribute('email'), 'beforeLoadHasBeenCalled')).toBe(true);\n      expect(hookTracker.get(user.getAttribute('email'), 'afterLoadHasBeenCalled')).toBe(true);\n      expect(hookTracker.get(user.getAttribute('fullName'), 'beforeLoadHasBeenCalled')).toBe(true);\n      expect(hookTracker.get(user.getAttribute('fullName'), 'afterLoadHasBeenCalled')).toBe(true);\n    });\n\n    test('beforeSave() and afterSave()', async () => {\n      const User = getUserClass();\n\n      const user = await User.fork().get('user1');\n\n      user.fullName = 'User 1 (modified)';\n\n      expect(hookTracker.get(user, 'beforeSaveHasBeenCalled')).toBeUndefined();\n      expect(hookTracker.get(user, 'afterSaveHasBeenCalled')).toBeUndefined();\n      expect(\n        hookTracker.get(user.getAttribute('email'), 'beforeSaveHasBeenCalled')\n      ).toBeUndefined();\n      expect(hookTracker.get(user.getAttribute('email'), 'afterSaveHasBeenCalled')).toBeUndefined();\n      expect(\n        hookTracker.get(user.getAttribute('fullName'), 'beforeSaveHasBeenCalled')\n      ).toBeUndefined();\n      expect(\n        hookTracker.get(user.getAttribute('fullName'), 'afterSaveHasBeenCalled')\n      ).toBeUndefined();\n\n      await user.save();\n\n      expect(hookTracker.get(user, 'beforeSaveHasBeenCalled')).toBe(true);\n      expect(hookTracker.get(user, 'afterSaveHasBeenCalled')).toBe(true);\n      expect(\n        hookTracker.get(user.getAttribute('email'), 'beforeSaveHasBeenCalled')\n      ).toBeUndefined();\n      expect(hookTracker.get(user.getAttribute('email'), 'afterSaveHasBeenCalled')).toBeUndefined();\n      expect(hookTracker.get(user.getAttribute('fullName'), 'beforeSaveHasBeenCalled')).toBe(true);\n      expect(hookTracker.get(user.getAttribute('fullName'), 'afterSaveHasBeenCalled')).toBe(true);\n    });\n\n    test('beforeDelete() and afterDelete()', async () => {\n      const User = getUserClass();\n\n      const user = await User.fork().get('user1');\n\n      expect(hookTracker.get(user, 'beforeDeleteHasBeenCalled')).toBeUndefined();\n      expect(hookTracker.get(user, 'afterDeleteHasBeenCalled')).toBeUndefined();\n      expect(\n        hookTracker.get(user.getAttribute('email'), 'beforeDeleteHasBeenCalled')\n      ).toBeUndefined();\n      expect(\n        hookTracker.get(user.getAttribute('email'), 'afterDeleteHasBeenCalled')\n      ).toBeUndefined();\n      expect(\n        hookTracker.get(user.getAttribute('fullName'), 'beforeDeleteHasBeenCalled')\n      ).toBeUndefined();\n      expect(\n        hookTracker.get(user.getAttribute('fullName'), 'afterDeleteHasBeenCalled')\n      ).toBeUndefined();\n\n      await user.delete();\n\n      expect(hookTracker.get(user, 'beforeDeleteHasBeenCalled')).toBe(true);\n      expect(hookTracker.get(user, 'afterDeleteHasBeenCalled')).toBe(true);\n      expect(hookTracker.get(user.getAttribute('email'), 'beforeDeleteHasBeenCalled')).toBe(true);\n      expect(hookTracker.get(user.getAttribute('email'), 'afterDeleteHasBeenCalled')).toBe(true);\n      expect(hookTracker.get(user.getAttribute('fullName'), 'beforeDeleteHasBeenCalled')).toBe(\n        true\n      );\n      expect(hookTracker.get(user.getAttribute('fullName'), 'afterDeleteHasBeenCalled')).toBe(true);\n    });\n\n    function getUserClass() {\n      class Picture extends BasePicture {}\n\n      class Organization extends BaseOrganization {}\n\n      class User extends BaseUser {\n        @provide() static Picture = Picture;\n        @provide() static Organization = Organization;\n\n        @secondaryIdentifier('string', {\n          beforeLoad(this: User) {\n            hookTracker.set(this.getAttribute('email'), 'beforeLoadHasBeenCalled', true);\n          },\n          afterLoad(this: User) {\n            hookTracker.set(this.getAttribute('email'), 'afterLoadHasBeenCalled', true);\n          },\n          beforeSave(this: User) {\n            hookTracker.set(this.getAttribute('email'), 'beforeSaveHasBeenCalled', true);\n          },\n          afterSave(this: User) {\n            hookTracker.set(this.getAttribute('email'), 'afterSaveHasBeenCalled', true);\n          },\n          beforeDelete(this: User) {\n            hookTracker.set(this.getAttribute('email'), 'beforeDeleteHasBeenCalled', true);\n          },\n          afterDelete(this: User) {\n            hookTracker.set(this.getAttribute('email'), 'afterDeleteHasBeenCalled', true);\n          }\n        })\n        email!: string;\n\n        @attribute('string', {\n          beforeLoad(this: User) {\n            hookTracker.set(this.getAttribute('fullName'), 'beforeLoadHasBeenCalled', true);\n          },\n          afterLoad(this: User) {\n            hookTracker.set(this.getAttribute('fullName'), 'afterLoadHasBeenCalled', true);\n          },\n          beforeSave(this: User) {\n            hookTracker.set(this.getAttribute('fullName'), 'beforeSaveHasBeenCalled', true);\n          },\n          afterSave(this: User) {\n            hookTracker.set(this.getAttribute('fullName'), 'afterSaveHasBeenCalled', true);\n          },\n          beforeDelete(this: User) {\n            hookTracker.set(this.getAttribute('fullName'), 'beforeDeleteHasBeenCalled', true);\n          },\n          afterDelete(this: User) {\n            hookTracker.set(this.getAttribute('fullName'), 'afterDeleteHasBeenCalled', true);\n          }\n        })\n        fullName!: string;\n\n        async beforeLoad(attributeSelector: AttributeSelector) {\n          await super.beforeLoad(attributeSelector);\n\n          hookTracker.set(this, 'beforeLoadHasBeenCalled', true);\n        }\n\n        async afterLoad(attributeSelector: AttributeSelector) {\n          await super.afterLoad(attributeSelector);\n\n          hookTracker.set(this, 'afterLoadHasBeenCalled', true);\n        }\n\n        async beforeSave(attributeSelector: AttributeSelector) {\n          await super.beforeSave(attributeSelector);\n\n          hookTracker.set(this, 'beforeSaveHasBeenCalled', true);\n        }\n\n        async afterSave(attributeSelector: AttributeSelector) {\n          await super.afterSave(attributeSelector);\n\n          hookTracker.set(this, 'afterSaveHasBeenCalled', true);\n        }\n\n        async beforeDelete(attributeSelector: AttributeSelector) {\n          await super.beforeDelete(attributeSelector);\n\n          hookTracker.set(this, 'beforeDeleteHasBeenCalled', true);\n        }\n\n        async afterDelete(attributeSelector: AttributeSelector) {\n          await super.afterDelete(attributeSelector);\n\n          hookTracker.set(this, 'afterDeleteHasBeenCalled', true);\n        }\n      }\n\n      const store = new MemoryStore({initialCollections: getInitialCollections()});\n\n      store.registerRootComponent(User);\n\n      return User;\n    }\n\n    const hookTrackerMap = new WeakMap<object, {[name: string]: any}>();\n\n    const hookTracker = {\n      get(target: object, name: string) {\n        return hookTrackerMap.get(target)?.[name];\n      },\n      set(target: object, name: string, value: any) {\n        let tracker = hookTrackerMap.get(target);\n        if (tracker === undefined) {\n          tracker = {};\n          hookTrackerMap.set(target, tracker);\n        }\n        tracker[name] = value;\n      }\n    };\n  });\n});\n"
  },
  {
    "path": "packages/integration-testing/tsconfig.json",
    "content": "{\n  \"extends\": \"@mvila/tsconfig\",\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/memory-navigator/README.md",
    "content": "# @layr/memory-navigator\n\nProvides a navigation system for a Layr app that is not running in a browser (e.g., an Electron app or a React Native app).\n\n## Installation\n\n```\nnpm install @layr/memory-navigator\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/memory-navigator/package.json",
    "content": "{\n  \"name\": \"@layr/memory-navigator\",\n  \"version\": \"2.0.59\",\n  \"description\": \"Provides a navigation system for a Layr app that is not running in a browser (e.g., an Electron app or a React Native app)\",\n  \"keywords\": [\n    \"layr\",\n    \"navigator\",\n    \"navigation\",\n    \"memory\"\n  ],\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/layrjs/layr/tree/master/packages/memory-navigator\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"dist/node-cjs/index.js\",\n  \"module\": \"dist/node-esm/index.js\",\n  \"engines\": {\n    \"node\": \">=16.0.0\"\n  },\n  \"scripts\": {\n    \"build\": \"dev-tools build:ts-library\",\n    \"prepare\": \"npm run build\",\n    \"publish:package\": \"dev-tools publish:package\",\n    \"update\": \"dev-tools update:dependencies\"\n  },\n  \"dependencies\": {\n    \"@layr/navigator\": \"^2.0.55\",\n    \"tslib\": \"^2.4.1\"\n  },\n  \"devDependencies\": {\n    \"@mvila/dev-tools\": \"^1.3.1\",\n    \"@mvila/tsconfig\": \"^1.0.6\"\n  }\n}\n"
  },
  {
    "path": "packages/memory-navigator/src/index.ts",
    "content": "export * from './memory-navigator';\n"
  },
  {
    "path": "packages/memory-navigator/src/memory-navigator.ts",
    "content": "import {Navigator, NavigatorOptions, normalizeURL} from '@layr/navigator';\n\nexport type MemoryNavigatorOptions = NavigatorOptions & {\n  initialURLs?: string[];\n  initialIndex?: number;\n};\n\n/**\n * *Inherits from [`Navigator`](https://layrjs.com/docs/v2/reference/navigator).*\n *\n * A [`Navigator`](https://layrjs.com/docs/v2/reference/navigator) that keeps the navigation history in memory. Useful in tests and non-browser environments like [React Native](https://reactnative.dev/).\n *\n * #### Usage\n *\n * Create a `MemoryNavigator` instance and register some [routable components](https://layrjs.com/docs/v2/reference/routable#routable-component-class) into it.\n */\nexport class MemoryNavigator extends Navigator {\n  _urls: URL[];\n  _index: number;\n\n  /**\n   * Creates a [`MemoryNavigator`](https://layrjs.com/docs/v2/reference/memory-navigator).\n   *\n   * @param [options.initialURLs] An array of URLs to populate the initial navigation history (default: `[]`).\n   * @param [options.initialIndex] A number specifying the current entry's index in the navigation history (default: the index of the last entry in the navigation history).\n   *\n   * @returns The [`MemoryNavigator`](https://layrjs.com/docs/v2/reference/memory-navigator) instance that was created.\n   *\n   * @category Creation\n   */\n  constructor(options: MemoryNavigatorOptions = {}) {\n    const {initialURLs = [], initialIndex = initialURLs.length - 1, ...otherOptions} = options;\n\n    super(otherOptions);\n\n    this._urls = initialURLs.map(normalizeURL);\n    this._index = initialIndex;\n  }\n\n  // === Current Location ===\n\n  /**\n   * See the methods that are inherited from the [`Navigator`](https://layrjs.com/docs/v2/reference/navigator#current-location) class.\n   *\n   * @category Current Location\n   */\n\n  _getCurrentURL() {\n    if (this._index === -1) {\n      throw new Error('The navigator has no current URL');\n    }\n\n    return this._urls[this._index];\n  }\n\n  // === Navigation ===\n\n  /**\n   * See the methods that are inherited from the [`Navigator`](https://layrjs.com/docs/v2/reference/navigator#navigation) class.\n   *\n   * @category Navigation\n   */\n\n  _navigate(url: URL) {\n    this._urls.splice(this._index + 1);\n    this._urls.push(url);\n    this._index++;\n  }\n\n  _redirect(url: URL) {\n    if (this._index === -1) {\n      throw new Error('The navigator has no current URL');\n    }\n\n    this._urls.splice(this._index);\n    this._urls.push(url);\n  }\n\n  _reload(_url: URL | undefined): void {\n    throw new Error(`The method 'reload() is not available in a memory navigator`);\n  }\n\n  _go(delta: number) {\n    let index = this._index;\n\n    index += delta;\n\n    if (index < 0 || index > this._urls.length - 1) {\n      throw new Error('Cannot go to an entry that does not exist in the navigator history');\n    }\n\n    this._index = index;\n  }\n\n  _getHistoryLength() {\n    return this._urls.length;\n  }\n\n  _getHistoryIndex() {\n    return this._index;\n  }\n\n  // === Observability ===\n\n  /**\n   * See the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) class.\n   *\n   * @category Observability\n   */\n}\n"
  },
  {
    "path": "packages/memory-navigator/tsconfig.json",
    "content": "{\n  \"extends\": \"@mvila/tsconfig\",\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/memory-store/README.md",
    "content": "# @layr/memory-store\n\nA Layr store using memory (useful for testing).\n\n## Installation\n\n```\nnpm install @layr/memory-store\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/memory-store/package.json",
    "content": "{\n  \"name\": \"@layr/memory-store\",\n  \"version\": \"2.0.82\",\n  \"description\": \"A Layr store using memory (useful for testing)\",\n  \"keywords\": [\n    \"layr\",\n    \"store\",\n    \"memory\"\n  ],\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/layrjs/layr/tree/master/packages/memory-store\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"dist/node-cjs/index.js\",\n  \"module\": \"dist/node-esm/index.js\",\n  \"engines\": {\n    \"node\": \">=16.0.0\"\n  },\n  \"scripts\": {\n    \"build\": \"dev-tools build:ts-library\",\n    \"prepare\": \"npm run build\",\n    \"publish:package\": \"dev-tools publish:package\",\n    \"update\": \"dev-tools update:dependencies\"\n  },\n  \"dependencies\": {\n    \"@layr/component\": \"^2.0.51\",\n    \"@layr/storable\": \"^2.0.76\",\n    \"@layr/store\": \"^2.0.81\",\n    \"core-helpers\": \"^1.0.8\",\n    \"lodash\": \"^4.17.21\",\n    \"sort-on\": \"^4.1.1\",\n    \"tslib\": \"^2.4.1\"\n  },\n  \"devDependencies\": {\n    \"@mvila/dev-tools\": \"^1.3.1\",\n    \"@mvila/tsconfig\": \"^1.0.6\",\n    \"@types/lodash\": \"^4.14.191\"\n  }\n}\n"
  },
  {
    "path": "packages/memory-store/src/index.ts",
    "content": "export * from './memory-store';\n"
  },
  {
    "path": "packages/memory-store/src/memory-store.ts",
    "content": "import {\n  Store,\n  CreateDocumentParams,\n  ReadDocumentParams,\n  UpdateDocumentParams,\n  DeleteDocumentParams,\n  FindDocumentsParams,\n  CountDocumentsParams,\n  MigrateCollectionParams,\n  MigrateCollectionResult,\n  Document,\n  Expression,\n  Path\n} from '@layr/store';\nimport type {Operator, SortDescriptor} from '@layr/storable';\nimport type {NormalizedIdentifierDescriptor} from '@layr/component';\nimport pull from 'lodash/pull';\nimport get from 'lodash/get';\nimport set from 'lodash/set';\nimport unset from 'lodash/unset';\nimport sortOn from 'sort-on';\n\ntype Collection = Document[];\n\ntype CollectionMap = {[name: string]: Collection};\n\n/**\n * *Inherits from [`Store`](https://layrjs.com/docs/v2/reference/store).*\n *\n * A [`Store`](https://layrjs.com/docs/v2/reference/store) that uses the memory to \"persist\" its registered [storable components](https://layrjs.com/docs/v2/reference/storable#storable-component-class). Since the stored data is wiped off every time the execution environment is restarted, a `MemoryStore` shouldn't be used for a real app.\n *\n * #### Usage\n *\n * Create a `MemoryStore` instance, register some [storable components](https://layrjs.com/docs/v2/reference/storable#storable-component-class) into it, and then use any [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storable-component-class)'s method to load, save, delete, or find components from the store.\n *\n * See an example of use in the [`MongoDBStore`](https://layrjs.com/docs/v2/reference/mongodb-store) class.\n */\nexport class MemoryStore extends Store {\n  /**\n   * Creates a [`MemoryStore`](https://layrjs.com/docs/v2/reference/memory-store).\n   *\n   * @param [options.initialCollections] A plain object specifying the initial data that should be populated into the store. The shape of the objet should be `{[collectionName]: documents}` where `collectionName` is the name of a [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class) class, and `documents` is an array of serialized storable component instances.\n   *\n   * @returns The [`MemoryStore`](https://layrjs.com/docs/v2/reference/memory-store) instance that was created.\n   *\n   * @example\n   * ```\n   * // Create an empty memory store\n   * const store = new MemoryStore();\n   *\n   * // Create a memory store with some initial data\n   * const store = new MemoryStore({\n   *   User: [\n   *     {\n   *       __component: 'User',\n   *       id: 'xyz789',\n   *       email: 'user@domain.com'\n   *     }\n   *   ],\n   *   Movie: [\n   *     {\n   *       __component: 'Movie',\n   *       id: 'abc123',\n   *       title: 'Inception'\n   *     }\n   *   ]\n   * });\n   * ```\n   *\n   * @category Creation\n   */\n  constructor(options: {initialCollections?: CollectionMap} = {}) {\n    const {initialCollections = {}, ...otherOptions} = options;\n\n    super(otherOptions);\n\n    this._collections = initialCollections;\n  }\n\n  // === Component Registration ===\n\n  /**\n   * See the methods that are inherited from the [`Store`](https://layrjs.com/docs/v2/reference/store#component-registration) class.\n   *\n   * @category Component Registration\n   */\n\n  // === Collections ===\n\n  _collections: CollectionMap;\n\n  _getCollection(name: string) {\n    let collection = this._collections[name];\n\n    if (collection === undefined) {\n      collection = [];\n      this._collections[name] = collection;\n    }\n\n    return collection;\n  }\n\n  // === Documents ===\n\n  async createDocument({collectionName, identifierDescriptor, document}: CreateDocumentParams) {\n    const collection = this._getCollection(collectionName);\n\n    const existingDocument = await this._readDocument({collection, identifierDescriptor});\n\n    if (existingDocument !== undefined) {\n      return false;\n    }\n\n    collection.push(document);\n\n    return true;\n  }\n\n  async readDocument({\n    collectionName,\n    identifierDescriptor\n  }: ReadDocumentParams): Promise<Document | undefined> {\n    const collection = this._getCollection(collectionName);\n\n    const document = await this._readDocument({collection, identifierDescriptor});\n\n    return document;\n  }\n\n  async _readDocument({\n    collection,\n    identifierDescriptor\n  }: {\n    collection: Collection;\n    identifierDescriptor: NormalizedIdentifierDescriptor;\n  }): Promise<Document | undefined> {\n    const [[identifierName, identifierValue]] = Object.entries(identifierDescriptor);\n\n    const document = collection.find((document) => document[identifierName] === identifierValue);\n\n    return document;\n  }\n\n  async updateDocument({\n    collectionName,\n    identifierDescriptor,\n    documentPatch\n  }: UpdateDocumentParams) {\n    const collection = this._getCollection(collectionName);\n\n    const existingDocument = await this._readDocument({collection, identifierDescriptor});\n\n    if (existingDocument === undefined) {\n      return false;\n    }\n\n    const {$set, $unset} = documentPatch;\n\n    if ($set !== undefined) {\n      for (const [path, value] of Object.entries($set)) {\n        set(existingDocument, path, value);\n      }\n    }\n\n    if ($unset !== undefined) {\n      for (const [path, value] of Object.entries($unset)) {\n        if (value) {\n          unset(existingDocument, path);\n        }\n      }\n    }\n\n    return true;\n  }\n\n  async deleteDocument({collectionName, identifierDescriptor}: DeleteDocumentParams) {\n    const collection = this._getCollection(collectionName);\n\n    const document = await this._readDocument({collection, identifierDescriptor});\n\n    if (document === undefined) {\n      return false;\n    }\n\n    pull(collection, document);\n\n    return true;\n  }\n\n  async findDocuments({\n    collectionName,\n    expressions,\n    sort,\n    skip,\n    limit\n  }: FindDocumentsParams): Promise<Document[]> {\n    const collection = this._getCollection(collectionName);\n\n    const documents = await this._findDocuments({collection, expressions, sort, skip, limit});\n\n    return documents;\n  }\n\n  async _findDocuments({\n    collection,\n    expressions,\n    sort,\n    skip,\n    limit\n  }: {\n    collection: Collection;\n    expressions: Expression[];\n    sort?: SortDescriptor;\n    skip?: number;\n    limit?: number;\n  }): Promise<Document[]> {\n    let documents = filterDocuments(collection, expressions);\n\n    documents = sortDocuments(documents, sort);\n\n    documents = skipDocuments(documents, skip);\n\n    documents = limitDocuments(documents, limit);\n\n    return documents;\n  }\n\n  async countDocuments({collectionName, expressions}: CountDocumentsParams) {\n    const collection = this._getCollection(collectionName);\n\n    const documents = await this._findDocuments({collection, expressions});\n\n    return documents.length;\n  }\n\n  // === Migration ===\n\n  async migrateCollection({collectionName}: MigrateCollectionParams) {\n    const result: MigrateCollectionResult = {\n      name: collectionName,\n      createdIndexes: [],\n      droppedIndexes: []\n    };\n\n    return result;\n  }\n}\n\nfunction filterDocuments(documents: Document[], expressions: Expression[]) {\n  if (expressions.length === 0) {\n    return documents; // Optimization\n  }\n\n  return documents.filter((document) => documentIsMatchingExpressions(document, expressions));\n}\n\nfunction documentIsMatchingExpressions(document: Document, expressions: Expression[]) {\n  for (const [path, operator, operand] of expressions) {\n    const attributeValue = path !== '' ? get(document, path) : document;\n\n    if (evaluateExpression(attributeValue, operator, operand, {path}) === false) {\n      return false;\n    }\n  }\n\n  return true;\n}\n\nfunction evaluateExpression(\n  attributeValue: any,\n  operator: Operator,\n  operand: any,\n  {path}: {path: Path}\n) {\n  // --- Basic operators ---\n\n  if (operator === '$equal') {\n    return attributeValue?.valueOf() === operand?.valueOf();\n  }\n\n  if (operator === '$notEqual') {\n    return attributeValue?.valueOf() !== operand?.valueOf();\n  }\n\n  if (operator === '$greaterThan') {\n    return attributeValue > operand;\n  }\n\n  if (operator === '$greaterThanOrEqual') {\n    return attributeValue >= operand;\n  }\n\n  if (operator === '$lessThan') {\n    return attributeValue < operand;\n  }\n\n  if (operator === '$lessThanOrEqual') {\n    return attributeValue <= operand;\n  }\n\n  if (operator === '$in') {\n    return operand.includes(attributeValue);\n  }\n\n  // --- String operators ---\n\n  if (operator === '$includes') {\n    if (typeof attributeValue !== 'string') {\n      return false;\n    }\n\n    return attributeValue.includes(operand);\n  }\n\n  if (operator === '$startsWith') {\n    if (typeof attributeValue !== 'string') {\n      return false;\n    }\n\n    return attributeValue.startsWith(operand);\n  }\n\n  if (operator === '$endsWith') {\n    if (typeof attributeValue !== 'string') {\n      return false;\n    }\n\n    return attributeValue.endsWith(operand);\n  }\n\n  if (operator === '$matches') {\n    if (typeof attributeValue !== 'string') {\n      return false;\n    }\n\n    return operand.test(attributeValue);\n  }\n\n  // --- Array operators ---\n\n  if (operator === '$some') {\n    if (!Array.isArray(attributeValue)) {\n      return false;\n    }\n\n    const subdocuments = attributeValue;\n    const subexpressions = operand;\n\n    return subdocuments.some((subdocument) =>\n      documentIsMatchingExpressions(subdocument, subexpressions)\n    );\n  }\n\n  if (operator === '$every') {\n    if (!Array.isArray(attributeValue)) {\n      return false;\n    }\n\n    const subdocuments = attributeValue;\n    const subexpressions = operand;\n\n    return subdocuments.every((subdocument) =>\n      documentIsMatchingExpressions(subdocument, subexpressions)\n    );\n  }\n\n  if (operator === '$length') {\n    if (!Array.isArray(attributeValue)) {\n      return false;\n    }\n\n    return attributeValue.length === operand;\n  }\n\n  // --- Logical operators ---\n\n  if (operator === '$not') {\n    const subexpressions = operand;\n\n    return !documentIsMatchingExpressions(attributeValue, subexpressions);\n  }\n\n  if (operator === '$and') {\n    const andSubexpressions = operand as any[];\n\n    return andSubexpressions.every((subexpressions) =>\n      documentIsMatchingExpressions(attributeValue, subexpressions)\n    );\n  }\n\n  if (operator === '$or') {\n    const orSubexpressions = operand as any[];\n\n    return orSubexpressions.some((subexpressions) =>\n      documentIsMatchingExpressions(attributeValue, subexpressions)\n    );\n  }\n\n  if (operator === '$nor') {\n    const norSubexpressions = operand as any[];\n\n    return !norSubexpressions.some((subexpressions) =>\n      documentIsMatchingExpressions(attributeValue, subexpressions)\n    );\n  }\n\n  throw new Error(\n    `A query contains an operator that is not supported (operator: '${operator}', path: '${path}')`\n  );\n}\n\nfunction sortDocuments(documents: Document[], sort: SortDescriptor | undefined) {\n  if (sort === undefined) {\n    return documents;\n  }\n\n  const properties = Object.entries(sort).map(([name, direction]) => {\n    let property = name;\n\n    if (direction.toLowerCase() === 'desc') {\n      property = `-${property}`;\n    }\n\n    return property;\n  });\n\n  return sortOn(documents, properties);\n}\n\nfunction skipDocuments(documents: Document[], skip: number | undefined) {\n  if (skip === undefined) {\n    return documents;\n  }\n\n  return documents.slice(skip);\n}\n\nfunction limitDocuments(documents: Document[], limit: number | undefined) {\n  if (limit === undefined) {\n    return documents;\n  }\n\n  return documents.slice(0, limit);\n}\n"
  },
  {
    "path": "packages/memory-store/tsconfig.json",
    "content": "{\n  \"extends\": \"@mvila/tsconfig\",\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/mongodb-store/README.md",
    "content": "# @layr/mongodb-store\n\nA Layr store for MongoDB.\n\n## Installation\n\n```\nnpm install @layr/mongodb-store\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/mongodb-store/package.json",
    "content": "{\n  \"name\": \"@layr/mongodb-store\",\n  \"version\": \"2.0.82\",\n  \"description\": \"A Layr store for MongoDB\",\n  \"keywords\": [\n    \"layr\",\n    \"store\",\n    \"persistence\",\n    \"storage\",\n    \"mongodb\"\n  ],\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/layrjs/layr/tree/master/packages/mongodb-store\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"dist/node-cjs/index.js\",\n  \"module\": \"dist/node-esm/index.js\",\n  \"engines\": {\n    \"node\": \">=16.0.0\"\n  },\n  \"scripts\": {\n    \"build\": \"dev-tools build:ts-library\",\n    \"prepare\": \"npm run build\",\n    \"test\": \"dev-tools test:ts-library\",\n    \"publish:package\": \"dev-tools publish:package\",\n    \"update\": \"dev-tools update:dependencies\"\n  },\n  \"dependencies\": {\n    \"@layr/component\": \"^2.0.51\",\n    \"@layr/storable\": \"^2.0.76\",\n    \"@layr/store\": \"^2.0.81\",\n    \"core-helpers\": \"^1.0.8\",\n    \"debug\": \"^4.3.4\",\n    \"lodash\": \"^4.17.21\",\n    \"microbatcher\": \"^2.0.8\",\n    \"mongodb\": \"^4.13.0\",\n    \"tslib\": \"^2.4.1\"\n  },\n  \"devDependencies\": {\n    \"@mvila/dev-tools\": \"^1.3.1\",\n    \"@mvila/tsconfig\": \"^1.0.6\",\n    \"@types/debug\": \"^4.1.7\",\n    \"@types/jest\": \"^29.2.5\",\n    \"@types/lodash\": \"^4.14.191\",\n    \"mongodb-memory-server\": \"^8.11.0\"\n  }\n}\n"
  },
  {
    "path": "packages/mongodb-store/src/index.ts",
    "content": "export * from './mongodb-store';\n"
  },
  {
    "path": "packages/mongodb-store/src/mongodb-store.test.ts",
    "content": "import {Component, provide} from '@layr/component';\nimport {\n  Storable,\n  StorableComponent,\n  primaryIdentifier,\n  secondaryIdentifier,\n  attribute\n} from '@layr/storable';\nimport {MongoMemoryServer} from 'mongodb-memory-server';\n\nimport {MongoDBStore} from './mongodb-store';\n\ndescribe('MongoDBStore', () => {\n  describe('Migration', () => {\n    let movieClass: typeof StorableComponent;\n    let server: MongoMemoryServer;\n    let store: MongoDBStore;\n\n    beforeEach(async () => {\n      class Person extends Storable(Component) {\n        @primaryIdentifier() id!: string;\n\n        @attribute('string') fullName!: string;\n      }\n\n      class Movie extends Storable(Component) {\n        @provide() static Person = Person;\n\n        @primaryIdentifier() id!: string;\n\n        @secondaryIdentifier() slug!: string;\n\n        @attribute('string') title!: string;\n\n        @attribute('number') year!: number;\n\n        @attribute('Person') director!: Person;\n      }\n\n      movieClass = Movie;\n\n      server = await MongoMemoryServer.create();\n\n      const connectionString = server.getUri();\n\n      store = new MongoDBStore(connectionString);\n\n      store.registerRootComponent(Movie);\n\n      await store.connect();\n    });\n\n    afterEach(async () => {\n      await store?.disconnect();\n\n      await server?.stop();\n    });\n\n    test('migrateStorables()', async () => {\n      let result = await store.migrateStorables({silent: true});\n\n      expect(result).toStrictEqual({\n        collections: [\n          {name: 'Movie', createdIndexes: ['slug [unique]', 'director.id'], droppedIndexes: []},\n          {name: 'Person', createdIndexes: [], droppedIndexes: []}\n        ]\n      });\n\n      result = await store.migrateStorables({silent: true});\n\n      expect(result).toStrictEqual({\n        collections: [\n          {name: 'Movie', createdIndexes: [], droppedIndexes: []},\n          {name: 'Person', createdIndexes: [], droppedIndexes: []}\n        ]\n      });\n\n      movieClass.prototype.deleteProperty('slug');\n\n      result = await store.migrateStorables({silent: true});\n\n      expect(result).toStrictEqual({\n        collections: [\n          {name: 'Movie', createdIndexes: [], droppedIndexes: ['slug [unique]']},\n          {name: 'Person', createdIndexes: [], droppedIndexes: []}\n        ]\n      });\n\n      movieClass.prototype.setIndex({title: 'asc'});\n\n      result = await store.migrateStorables({silent: true});\n\n      expect(result).toStrictEqual({\n        collections: [\n          {name: 'Movie', createdIndexes: ['title'], droppedIndexes: []},\n          {name: 'Person', createdIndexes: [], droppedIndexes: []}\n        ]\n      });\n\n      movieClass.prototype.setIndex({title: 'asc'}, {isUnique: true});\n\n      result = await store.migrateStorables({silent: true});\n\n      expect(result).toStrictEqual({\n        collections: [\n          {name: 'Movie', createdIndexes: ['title [unique]'], droppedIndexes: ['title']},\n          {name: 'Person', createdIndexes: [], droppedIndexes: []}\n        ]\n      });\n\n      movieClass.prototype.deleteIndex({title: 'asc'});\n\n      result = await store.migrateStorables({silent: true});\n\n      expect(result).toStrictEqual({\n        collections: [\n          {name: 'Movie', createdIndexes: [], droppedIndexes: ['title [unique]']},\n          {name: 'Person', createdIndexes: [], droppedIndexes: []}\n        ]\n      });\n\n      movieClass.prototype.setIndex({year: 'desc', title: 'asc'}, {isUnique: true});\n\n      result = await store.migrateStorables({silent: true});\n\n      expect(result).toStrictEqual({\n        collections: [\n          {name: 'Movie', createdIndexes: ['year (desc) + title [unique]'], droppedIndexes: []},\n          {name: 'Person', createdIndexes: [], droppedIndexes: []}\n        ]\n      });\n\n      movieClass.prototype.setIndex({year: 'asc', id: 'asc'});\n\n      result = await store.migrateStorables({silent: true});\n\n      expect(result).toStrictEqual({\n        collections: [\n          {name: 'Movie', createdIndexes: ['year + _id'], droppedIndexes: []},\n          {name: 'Person', createdIndexes: [], droppedIndexes: []}\n        ]\n      });\n    });\n  });\n\n  describe('Document operations', () => {\n    let server: MongoMemoryServer;\n    let store: MongoDBStore;\n\n    beforeEach(async () => {\n      server = await MongoMemoryServer.create({instance: {storageEngine: 'wiredTiger'}});\n\n      const connectionString = server.getUri();\n\n      store = new MongoDBStore(connectionString);\n\n      await store.connect();\n\n      await store.migrateCollection({\n        collectionName: 'Movie',\n        collectionSchema: {\n          indexes: [\n            {attributes: {_id: 'asc'}, isPrimary: true, isUnique: true},\n            {attributes: {slug: 'asc'}, isPrimary: false, isUnique: true},\n            {attributes: {title: 'asc'}, isPrimary: false, isUnique: false},\n            {attributes: {year: 'desc'}, isPrimary: false, isUnique: false},\n            {attributes: {year: 'desc', title: 'asc'}, isPrimary: false, isUnique: true},\n            {attributes: {tags: 'asc'}, isPrimary: false, isUnique: false}\n          ]\n        },\n        silent: true\n      });\n    });\n\n    afterEach(async () => {\n      await store?.disconnect();\n\n      await server?.stop();\n    });\n\n    test('createDocument()', async () => {\n      expect(\n        await store.createDocument({\n          collectionName: 'Movie',\n          identifierDescriptor: {_id: 'movie1'},\n          document: {\n            __component: 'Movie',\n            _id: 'movie1',\n            slug: 'inception',\n            title: 'Inception',\n            year: 2010,\n            tags: ['action', 'drama']\n          }\n        })\n      ).toBe(true);\n\n      expect(\n        await store.createDocument({\n          collectionName: 'Movie',\n          identifierDescriptor: {_id: 'movie1'},\n          document: {\n            __component: 'Movie',\n            _id: 'movie1',\n            slug: 'inception-2',\n            title: 'Inception 2',\n            year: 2010,\n            tags: ['action', 'drama']\n          }\n        })\n      ).toBe(false);\n\n      await expect(\n        store.createDocument({\n          collectionName: 'Movie',\n          identifierDescriptor: {_id: 'movie1'},\n          document: {\n            __component: 'Movie',\n            _id: 'movie2',\n            slug: 'inception',\n            title: 'Inception',\n            year: 2010,\n            tags: ['action', 'drama']\n          }\n        })\n      ).rejects.toThrow(\n        \"A duplicate key error occurred while creating a MongoDB document (collection: 'Movie', index: 'slug [unique]')\"\n      );\n\n      await expect(\n        store.createDocument({\n          collectionName: 'Movie',\n          identifierDescriptor: {_id: 'movie1'},\n          document: {\n            __component: 'Movie',\n            _id: 'movie2',\n            slug: 'inception-2',\n            title: 'Inception',\n            year: 2010,\n            tags: ['action', 'drama']\n          }\n        })\n      ).rejects.toThrow(\n        \"A duplicate key error occurred while creating a MongoDB document (collection: 'Movie', index: 'year (desc) + title [unique]')\"\n      );\n    });\n\n    test('updateDocument()', async () => {\n      await store.createDocument({\n        collectionName: 'Movie',\n        identifierDescriptor: {_id: 'movie1'},\n        document: {\n          __component: 'Movie',\n          _id: 'movie1',\n          slug: 'inception',\n          title: 'Inception',\n          year: 2010,\n          tags: ['action', 'drama']\n        }\n      });\n\n      await store.createDocument({\n        collectionName: 'Movie',\n        identifierDescriptor: {_id: 'movie2'},\n        document: {\n          __component: 'Movie',\n          _id: 'movie2',\n          slug: 'inception-2',\n          title: 'Inception 2',\n          year: 2020,\n          tags: ['action', 'drama']\n        }\n      });\n\n      expect(\n        await store.updateDocument({\n          collectionName: 'Movie',\n          identifierDescriptor: {_id: 'movie2'},\n          documentPatch: {$set: {year: 2021}}\n        })\n      ).toBe(true);\n\n      expect(\n        await store.updateDocument({\n          collectionName: 'Movie',\n          identifierDescriptor: {_id: 'movie3'},\n          documentPatch: {$set: {year: 2021}}\n        })\n      ).toBe(false);\n\n      await expect(\n        store.updateDocument({\n          collectionName: 'Movie',\n          identifierDescriptor: {_id: 'movie2'},\n          documentPatch: {$set: {slug: 'inception'}}\n        })\n      ).rejects.toThrow(\n        \"A duplicate key error occurred while updating a MongoDB document (collection: 'Movie', index: 'slug [unique]')\"\n      );\n\n      await expect(\n        store.updateDocument({\n          collectionName: 'Movie',\n          identifierDescriptor: {_id: 'movie2'},\n          documentPatch: {$set: {title: 'Inception', year: 2010}}\n        })\n      ).rejects.toThrow(\n        \"A duplicate key error occurred while updating a MongoDB document (collection: 'Movie', index: 'year (desc) + title [unique]')\"\n      );\n    });\n\n    test('findDocuments()', async () => {\n      await store.createDocument({\n        collectionName: 'Movie',\n        identifierDescriptor: {_id: 'movie1'},\n        document: {\n          __component: 'Movie',\n          _id: 'movie1',\n          slug: 'inception',\n          title: 'Inception',\n          year: 2010,\n          tags: ['action', 'adventure', 'sci-fi']\n        }\n      });\n\n      await store.createDocument({\n        collectionName: 'Movie',\n        identifierDescriptor: {_id: 'movie2'},\n        document: {\n          __component: 'Movie',\n          _id: 'movie2',\n          slug: 'forrest-gump',\n          title: 'Forrest Gump',\n          year: 1994,\n          tags: ['drama', 'romance']\n        }\n      });\n\n      await store.createDocument({\n        collectionName: 'Movie',\n        identifierDescriptor: {_id: 'movie3'},\n        document: {\n          __component: 'Movie',\n          _id: 'movie3',\n          slug: 'leon',\n          title: 'Léon',\n          year: 1994,\n          tags: ['action', 'crime', 'drama']\n        }\n      });\n\n      await store.createDocument({\n        collectionName: 'Movie',\n        identifierDescriptor: {_id: 'movie4'},\n        document: {\n          __component: 'Movie',\n          _id: 'movie4',\n          slug: 'unknown',\n          title: 'Unknown',\n          year: 0,\n          tags: []\n        }\n      });\n\n      expect(\n        await store.findDocuments({\n          collectionName: 'Movie',\n          expressions: [],\n          projection: {_id: 1},\n          sort: {_id: 'asc'}\n        })\n      ).toStrictEqual([{_id: 'movie1'}, {_id: 'movie2'}, {_id: 'movie3'}, {_id: 'movie4'}]);\n\n      // --- $in ---\n\n      expect(\n        await store.findDocuments({\n          collectionName: 'Movie',\n          // @ts-ignore\n          expressions: [['tags', '$in', ['romance']]],\n          projection: {_id: 1},\n          sort: {_id: 'asc'}\n        })\n      ).toStrictEqual([{_id: 'movie2'}]);\n\n      expect(\n        await store.findDocuments({\n          collectionName: 'Movie',\n          // @ts-ignore\n          expressions: [['tags', '$in', ['action']]],\n          projection: {_id: 1},\n          sort: {_id: 'asc'}\n        })\n      ).toStrictEqual([{_id: 'movie1'}, {_id: 'movie3'}]);\n\n      // --- $not $in ---\n\n      expect(\n        await store.findDocuments({\n          collectionName: 'Movie',\n          // @ts-ignore\n          expressions: [['tags', '$not', [['', '$in', ['action']]]]],\n          projection: {_id: 1},\n          sort: {_id: 'asc'}\n        })\n      ).toStrictEqual([{_id: 'movie2'}, {_id: 'movie4'}]);\n\n      expect(\n        await store.findDocuments({\n          collectionName: 'Movie',\n          // @ts-ignore\n          expressions: [['tags', '$not', [['', '$in', ['action', 'drama']]]]],\n          projection: {_id: 1},\n          sort: {_id: 'asc'}\n        })\n      ).toStrictEqual([{_id: 'movie4'}]);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/mongodb-store/src/mongodb-store.ts",
    "content": "import {\n  Store,\n  CreateDocumentParams,\n  ReadDocumentParams,\n  UpdateDocumentParams,\n  DeleteDocumentParams,\n  FindDocumentsParams,\n  CountDocumentsParams,\n  MigrateCollectionParams,\n  MigrateCollectionResult,\n  Document,\n  Expression,\n  Path,\n  Operand\n} from '@layr/store';\nimport type {\n  StorableComponent,\n  Query,\n  SortDescriptor,\n  SortDirection,\n  Operator\n} from '@layr/storable';\nimport {ensureComponentInstance} from '@layr/component';\nimport {MongoClient, Db, Collection, Filter, FindOptions} from 'mongodb';\nimport {Microbatcher, Operation} from 'microbatcher';\nimport {hasOwnProperty, assertIsObjectLike} from 'core-helpers';\nimport isEmpty from 'lodash/isEmpty';\nimport mapKeys from 'lodash/mapKeys';\nimport mapValues from 'lodash/mapValues';\nimport escapeRegExp from 'lodash/escapeRegExp';\nimport groupBy from 'lodash/groupBy';\nimport debugModule from 'debug';\n\nconst debug = debugModule('layr:mongodb-store');\n// To display the debug log, set this environment:\n// DEBUG=layr:mongodb-store DEBUG_DEPTH=10\n\nconst MONGODB_PRIMARY_IDENTIFIER_ATTRIBUTE_NAME = '_id';\nconst MONGODB_PRIMARY_IDENTIFIER_ATTRIBUTE_INDEX_NAME = '_id_';\n\n/**\n * *Inherits from [`Store`](https://layrjs.com/docs/v2/reference/store).*\n *\n * A [`Store`](https://layrjs.com/docs/v2/reference/store) that uses a [MongoDB](https://www.mongodb.com/) database to persist its registered [storable components](https://layrjs.com/docs/v2/reference/storable#storable-component-class).\n *\n * #### Usage\n *\n * Create a `MongoDBStore` instance, register some [storable components](https://layrjs.com/docs/v2/reference/storable#storable-component-class) into it, and then use any [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storable-component-class)'s method to load, save, delete, or find components from the store.\n *\n * For example, let's build a simple `Backend` that provides a `Movie` component.\n *\n * First, let's define the components that we are going to use:\n *\n * ```\n * // JS\n *\n * import {Component} from '﹫layr/component';\n * import {Storable, primaryIdentifier, attribute} from '@layr/storable';\n *\n * class Movie extends Storable(Component) {\n *   @primaryIdentifier() id;\n *\n *   @attribute() title = '';\n * }\n *\n * class Backend extends Component {\n *   ﹫provide() static Movie = Movie;\n * }\n * ```\n *\n * ```\n * // TS\n *\n * import {Component} from '﹫layr/component';\n * import {Storable, primaryIdentifier, attribute} from '@layr/storable';\n *\n * class Movie extends Storable(Component) {\n *   @primaryIdentifier() id!: string;\n *\n *   @attribute() title = '';\n * }\n *\n * class Backend extends Component {\n *   ﹫provide() static Movie = Movie;\n * }\n * ```\n *\n * Next, let's create a `MongoDBStore` instance, and let's register the `Backend` component as the root component of the store:\n *\n * ```\n * import {MongoDBStore} from '﹫layr/mongodb-store';\n *\n * const store = new MongoDBStore('mongodb://user:pass@host:port/db');\n *\n * store.registerRootComponent(Backend);\n * ```\n *\n * Finally, we can interact with the store by calling some [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storable-component-class) methods:\n *\n * ```\n * let movie = new Movie({id: 'abc123', title: 'Inception'});\n *\n * // Save the movie to the store\n * await movie.save();\n *\n * // Get the movie from the store\n * movie = await Movie.get('abc123');\n * movie.title; // => 'Inception'\n *\n * // Modify the movie, and save it to the store\n * movie.title = 'Inception 2';\n * await movie.save();\n *\n * // Find the movies that have a title starting with 'Inception'\n * const movies = await Movie.find({title: {$startsWith: 'Inception'}});\n * movies.length; // => 1 (one movie found)\n * movies[0].title; // => 'Inception 2'\n * movies[0] === movie; // => true (thanks to the identity mapping)\n *\n * // Delete the movie from the store\n * await movie.delete();\n * ```\n */\nexport class MongoDBStore extends Store {\n  private _connectionString: string;\n  private _poolSize: number;\n\n  /**\n   * Creates a [`MongoDBStore`](https://layrjs.com/docs/v2/reference/mongodb-store).\n   *\n   * @param connectionString The [connection string](https://docs.mongodb.com/manual/reference/connection-string/) of the MongoDB database to use.\n   * @param [options.poolSize] A number specifying the maximum size of the connection pool (default: `1`).\n   *\n   * @returns The [`MongoDBStore`](https://layrjs.com/docs/v2/reference/mongodb-store) instance that was created.\n   *\n   * @example\n   * ```\n   * const store = new MongoDBStore('mongodb://user:pass@host:port/db');\n   * ```\n   *\n   * @category Creation\n   */\n  constructor(connectionString: string, options: {poolSize?: number} = {}) {\n    if (typeof connectionString !== 'string') {\n      throw new Error(\n        `Expected a 'connectionString' to create a MongoDBStore, but received a value of type '${typeof connectionString}'`\n      );\n    }\n\n    if (connectionString.length === 0) {\n      throw new Error(\n        `Expected a 'connectionString' to create a MongoDBStore, but received an empty string`\n      );\n    }\n\n    const {poolSize = 1, ...otherOptions} = options;\n\n    super(otherOptions);\n\n    this._connectionString = connectionString;\n    this._poolSize = poolSize;\n  }\n\n  getURL() {\n    return this._connectionString;\n  }\n\n  // === Component Registration ===\n\n  /**\n   * See the methods that are inherited from the [`Store`](https://layrjs.com/docs/v2/reference/store#component-registration) class.\n   *\n   * @category Component Registration\n   */\n\n  // === Connection ===\n\n  /**\n   * Initiates a connection to the MongoDB database.\n   *\n   * Since this method is called automatically when you interact with the store through any of the [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storable-component-class) methods, you shouldn't have to call it manually.\n   *\n   * @category Connection to MongoDB\n   */\n  async connect() {\n    await this._connectClient();\n  }\n\n  /**\n   * Closes the connection to the MongoDB database. Unless you are building a tool that uses a store for an ephemeral duration, you shouldn't have to call this method.\n   *\n   * @category Connection to MongoDB\n   */\n  async disconnect() {\n    await this._disconnectClient();\n  }\n\n  // === Documents ===\n\n  async createDocument({collectionName, document}: CreateDocumentParams) {\n    const collection = await this._getCollection(collectionName);\n\n    try {\n      const {acknowledged} = await debugCall(\n        async () => {\n          const {acknowledged} = await collection.insertOne(document);\n\n          return {acknowledged};\n        },\n        'db.%s.insertOne(%o)',\n        collectionName,\n        document\n      );\n\n      return acknowledged;\n    } catch (error: any) {\n      if (error.name === 'MongoServerError' && error.code === 11000) {\n        const matches = error.message.match(/ index: (.*) dup key/);\n\n        if (matches === null) {\n          throw error;\n        }\n\n        const indexName = matches[1];\n\n        if (indexName === MONGODB_PRIMARY_IDENTIFIER_ATTRIBUTE_INDEX_NAME) {\n          return false; // The document already exists\n        }\n\n        throw Object.assign(\n          new Error(\n            `A duplicate key error occurred while creating a MongoDB document (collection: '${collectionName}', index: '${indexName}')`\n          ),\n          {code: 'DUPLICATE_KEY_ERROR', collectionName, indexName}\n        );\n      }\n\n      throw error;\n    }\n  }\n\n  async readDocument({\n    collectionName,\n    identifierDescriptor,\n    projection\n  }: ReadDocumentParams): Promise<Document | undefined> {\n    const collection = await this._getCollection(collectionName);\n\n    const query = identifierDescriptor;\n    const options = {projection};\n\n    const document: Document | null = await debugCall(\n      async () => await batchableFindOne(collection, query, options),\n      'db.%s.batchableFindOne(%o, %o)',\n      collectionName,\n      query,\n      options\n    );\n\n    if (document === null) {\n      return undefined;\n    }\n\n    return document;\n  }\n\n  async updateDocument({\n    collectionName,\n    identifierDescriptor,\n    documentPatch\n  }: UpdateDocumentParams) {\n    const collection = await this._getCollection(collectionName);\n\n    const filter = identifierDescriptor;\n\n    const {matchedCount} = await debugCall(\n      async () => {\n        try {\n          const {matchedCount, modifiedCount} = await collection.updateOne(filter, documentPatch);\n\n          return {matchedCount, modifiedCount};\n        } catch (error: any) {\n          if (error.name === 'MongoServerError' && error.code === 11000) {\n            const matches = error.message.match(/ index: (.*) dup key/);\n\n            if (matches === null) {\n              throw error;\n            }\n\n            const indexName = matches[1];\n\n            throw Object.assign(\n              new Error(\n                `A duplicate key error occurred while updating a MongoDB document (collection: '${collectionName}', index: '${indexName}')`\n              ),\n              {code: 'DUPLICATE_KEY_ERROR', collectionName, indexName}\n            );\n          }\n\n          throw error;\n        }\n      },\n      'db.%s.updateOne(%o, %o)',\n      collectionName,\n      filter,\n      documentPatch\n    );\n\n    return matchedCount === 1;\n  }\n\n  async deleteDocument({collectionName, identifierDescriptor}: DeleteDocumentParams) {\n    const collection = await this._getCollection(collectionName);\n\n    const filter = identifierDescriptor;\n\n    const {deletedCount} = await debugCall(\n      async () => {\n        const {deletedCount} = await collection.deleteOne(filter);\n\n        return {deletedCount};\n      },\n      'db.%s.deleteOne(%o)',\n      collectionName,\n      filter\n    );\n\n    return deletedCount === 1;\n  }\n\n  async findDocuments({\n    collectionName,\n    expressions,\n    projection,\n    sort,\n    skip,\n    limit\n  }: FindDocumentsParams): Promise<Document[]> {\n    const collection = await this._getCollection(collectionName);\n\n    const mongoQuery = buildMongoQuery(expressions);\n    const mongoSort = buildMongoSort(sort);\n\n    const options = {projection};\n\n    const documents: Document[] = await debugCall(\n      async () => {\n        const cursor = collection.find(mongoQuery, options);\n\n        if (mongoSort !== undefined) {\n          cursor.sort(mongoSort);\n        }\n\n        if (skip !== undefined) {\n          cursor.skip(skip);\n        }\n\n        if (limit !== undefined) {\n          cursor.limit(limit);\n        }\n\n        const documents = await cursor.toArray();\n\n        return documents;\n      },\n      'db.%s.find(%o, %o)',\n      collectionName,\n      mongoQuery,\n      options\n    );\n\n    return documents;\n  }\n\n  async countDocuments({collectionName, expressions}: CountDocumentsParams) {\n    const collection = await this._getCollection(collectionName);\n\n    const query = buildMongoQuery(expressions);\n\n    const documentsCount = await debugCall(\n      async () => {\n        const documentsCount = await collection.countDocuments(query);\n\n        return documentsCount;\n      },\n      'db.%s.countDocuments(%o)',\n      collectionName,\n      query\n    );\n\n    return documentsCount;\n  }\n\n  // === Serialization ===\n\n  toDocument<Value>(storable: typeof StorableComponent | StorableComponent, value: Value) {\n    let document = super.toDocument(storable, value);\n\n    if (typeof document === 'object') {\n      const primaryIdentifierAttributeName = ensureComponentInstance(storable)\n        .getPrimaryIdentifierAttribute()\n        .getName();\n\n      document = mapKeys(document as any, (_, name) =>\n        name === primaryIdentifierAttributeName ? MONGODB_PRIMARY_IDENTIFIER_ATTRIBUTE_NAME : name\n      ) as Value;\n    }\n\n    return document;\n  }\n\n  fromDocument(\n    storable: typeof StorableComponent | StorableComponent,\n    document: Document\n  ): Document {\n    let serializedStorable = super.fromDocument(storable, document);\n\n    if (typeof serializedStorable === 'object') {\n      const primaryIdentifierAttributeName = ensureComponentInstance(storable)\n        .getPrimaryIdentifierAttribute()\n        .getName();\n\n      serializedStorable = mapKeys(serializedStorable, (_, name) =>\n        name === MONGODB_PRIMARY_IDENTIFIER_ATTRIBUTE_NAME ? primaryIdentifierAttributeName : name\n      );\n    }\n\n    return serializedStorable;\n  }\n\n  // === Migration ===\n\n  /**\n   * See the methods that are inherited from the [`Store`](https://layrjs.com/docs/v2/reference/store#migration) class.\n   *\n   * @category Migration\n   */\n\n  async migrateCollection({\n    collectionName,\n    collectionSchema,\n    silent = false\n  }: MigrateCollectionParams) {\n    const result: MigrateCollectionResult = {\n      name: collectionName,\n      createdIndexes: [],\n      droppedIndexes: []\n    };\n\n    const database = await this._getDatabase();\n\n    let collection: Collection;\n    let collectionHasBeenCreated: boolean;\n\n    const collections = await database\n      .listCollections({name: collectionName}, {nameOnly: true})\n      .toArray();\n\n    if (collections.length === 0) {\n      if (!silent) {\n        console.log(`Creating collection: '${collectionName}'`);\n      }\n\n      collection = await database.createCollection(collectionName);\n      collectionHasBeenCreated = true;\n    } else {\n      collection = database.collection(collectionName);\n      collectionHasBeenCreated = false;\n    }\n\n    const existingIndexNames: string[] = (await collection.indexes()).map(\n      (index: any) => index.name\n    );\n\n    const indexesToEnsure: any[] = [];\n\n    for (const index of collectionSchema.indexes) {\n      let indexName = '';\n      let indexSpec: any = {};\n\n      for (let [name, direction] of Object.entries(index.attributes)) {\n        const directionString = direction.toLowerCase();\n\n        if (indexName !== '') {\n          indexName += ' + ';\n        }\n\n        indexName += name;\n\n        if (directionString === 'desc') {\n          indexName += ' (desc)';\n        }\n\n        indexSpec[name] = directionString === 'desc' ? -1 : 1;\n      }\n\n      if (index.isUnique) {\n        indexName += ' [unique]';\n      }\n\n      if (indexName === `${MONGODB_PRIMARY_IDENTIFIER_ATTRIBUTE_NAME} [unique]`) {\n        indexName = MONGODB_PRIMARY_IDENTIFIER_ATTRIBUTE_INDEX_NAME;\n      }\n\n      indexesToEnsure.push({name: indexName, spec: indexSpec, isUnique: index.isUnique});\n    }\n\n    const indexesToCreate = indexesToEnsure.filter(\n      (index) => !existingIndexNames.includes(index.name)\n    );\n\n    const indexNamesToDrop = existingIndexNames.filter(\n      (name) =>\n        name !== MONGODB_PRIMARY_IDENTIFIER_ATTRIBUTE_INDEX_NAME &&\n        !indexesToEnsure.some((index) => index.name === name)\n    );\n\n    if (indexesToCreate.length !== 0 || indexNamesToDrop.length !== 0) {\n      if (!collectionHasBeenCreated && !silent) {\n        console.log(`Migrating collection: '${collectionName}'`);\n      }\n    }\n\n    for (const name of indexNamesToDrop) {\n      if (!silent) {\n        console.log(`- Dropping index: '${name}'`);\n      }\n\n      await collection.dropIndex(name);\n\n      result.droppedIndexes.push(name);\n    }\n\n    for (const index of indexesToCreate) {\n      if (!silent) {\n        console.log(`- Creating index: '${index.name}'`);\n      }\n\n      await collection.createIndex(index.spec, {name: index.name, unique: index.isUnique});\n\n      result.createdIndexes.push(index.name);\n    }\n\n    return result;\n  }\n\n  // === MongoDB client ===\n\n  private _client: MongoClient | undefined;\n\n  private async _getClient() {\n    await this._connectClient();\n\n    return this._client!;\n  }\n\n  private _connectClientPromise: Promise<void> | undefined;\n\n  private _connectClient() {\n    // This method memoize the ongoing promise to allow concurrent execution\n\n    if (this._connectClientPromise !== undefined) {\n      return this._connectClientPromise;\n    }\n\n    this._connectClientPromise = (async () => {\n      try {\n        if (this._client === undefined) {\n          debug(`Connecting to MongoDB Server (connectionString: ${this._connectionString})...`);\n\n          this._client = await MongoClient.connect(\n            this._fixConnectionString(this._connectionString),\n            {\n              maxPoolSize: this._poolSize\n            }\n          );\n\n          debug(`Connected to MongoDB Server (connectionString: ${this._connectionString})`);\n        }\n      } finally {\n        this._connectClientPromise = undefined;\n      }\n    })();\n\n    return this._connectClientPromise;\n  }\n\n  private _fixConnectionString(connectionString: string) {\n    // Fix an issue when localhost resolves to an IPv6 loopback address (::1)\n    //\n    // It happens in the following environment:\n    // - macOS v13.0.1\n    // - Node.js v18.12.1\n\n    const connectionStringURL = new URL(connectionString);\n\n    if (connectionStringURL.hostname !== 'localhost') {\n      return connectionString;\n    }\n\n    connectionStringURL.hostname = '127.0.0.1';\n\n    return connectionStringURL.toString();\n  }\n\n  private async _disconnectClient() {\n    if (this._connectClientPromise !== undefined) {\n      // If the connection is ongoing, let's wait it finishes before disconnecting\n      try {\n        await this._connectClientPromise;\n      } catch {\n        // NOOP\n      }\n    }\n\n    const client = this._client;\n\n    if (client !== undefined) {\n      // Unset `this._client` and `this._db` early to avoid issue in case of concurrent execution\n      this._client = undefined;\n      this._db = undefined;\n\n      debug(`Disconnecting from MongoDB Server (connectionString: ${this._connectionString})...`);\n\n      await client.close();\n\n      debug(`Disconnected from MongoDB Server (connectionString: ${this._connectionString})`);\n    }\n  }\n\n  private _db: Db | undefined;\n\n  private async _getDatabase() {\n    if (!this._db) {\n      const client = await this._getClient();\n      this._db = client.db();\n    }\n\n    return this._db;\n  }\n\n  private _collections: {[name: string]: Collection} | undefined;\n\n  private async _getCollection(name: string) {\n    if (this._collections === undefined) {\n      this._collections = Object.create(null);\n    }\n\n    if (this._collections![name] === undefined) {\n      const database = await this._getDatabase();\n      this._collections![name] = database.collection(name);\n    }\n\n    return this._collections![name];\n  }\n}\n\nfunction buildMongoQuery(expressions: Expression[]) {\n  const query: Query = {};\n\n  for (const [path, operator, value] of expressions) {\n    let subquery: Query;\n\n    if (path !== '') {\n      subquery = query[path];\n\n      if (subquery === undefined) {\n        subquery = {};\n        query[path] = subquery;\n      }\n    } else {\n      subquery = query;\n    }\n\n    const [actualOperator, actualValue] = handleOperator(operator, value, {path});\n\n    subquery[actualOperator] = actualValue;\n  }\n\n  return query;\n}\n\nfunction handleOperator(\n  operator: Operator,\n  value: Operand,\n  {path}: {path: Path}\n): [Operator, unknown] {\n  // --- Basic operators ---\n\n  if (operator === '$equal') {\n    return ['$eq', value];\n  }\n\n  if (operator === '$notEqual') {\n    return ['$ne', value];\n  }\n\n  if (operator === '$greaterThan') {\n    return ['$gt', value];\n  }\n\n  if (operator === '$greaterThanOrEqual') {\n    return ['$gte', value];\n  }\n\n  if (operator === '$lessThan') {\n    return ['$lt', value];\n  }\n\n  if (operator === '$lessThanOrEqual') {\n    return ['$lte', value];\n  }\n\n  if (operator === '$in') {\n    return ['$in', value];\n  }\n\n  // --- String operators ---\n\n  if (operator === '$includes') {\n    return ['$regex', escapeRegExp(value as string)];\n  }\n\n  if (operator === '$startsWith') {\n    return ['$regex', `^${escapeRegExp(value as string)}`];\n  }\n\n  if (operator === '$endsWith') {\n    return ['$regex', `${escapeRegExp(value as string)}$`];\n  }\n\n  if (operator === '$matches') {\n    return ['$regex', value];\n  }\n\n  // --- Array operators ---\n\n  if (operator === '$some') {\n    const subexpressions = value as Expression[];\n    const subquery = buildMongoQuery(subexpressions);\n    return ['$elemMatch', subquery];\n  }\n\n  if (operator === '$every') {\n    // TODO: Make it works for complex queries (regexps, array of objects, etc.)\n    const subexpressions = value as Expression[];\n    const subquery = buildMongoQuery(subexpressions);\n    return ['$not', {$elemMatch: {$not: subquery}}];\n  }\n\n  if (operator === '$length') {\n    return ['$size', value];\n  }\n\n  // --- Logical operators ---\n\n  if (operator === '$not') {\n    const subexpressions = value as Expression[];\n    const subquery = buildMongoQuery(subexpressions);\n    return ['$not', subquery];\n  }\n\n  if (operator === '$and') {\n    const andSubexpressions = value as Expression[][];\n    const andSubqueries = andSubexpressions.map((subexpressions) =>\n      buildMongoQuery(subexpressions)\n    );\n    return ['$and', andSubqueries];\n  }\n\n  if (operator === '$or') {\n    const orSubexpressions = value as Expression[][];\n    const orSubqueries = orSubexpressions.map((subexpressions) => buildMongoQuery(subexpressions));\n    return ['$or', orSubqueries];\n  }\n\n  if (operator === '$nor') {\n    const norSubexpressions = value as Expression[][];\n    const norSubqueries = norSubexpressions.map((subexpressions) =>\n      buildMongoQuery(subexpressions)\n    );\n    return ['$nor', norSubqueries];\n  }\n\n  throw new Error(\n    `A query contains an operator that is not supported (operator: '${operator}', path: '${path}')`\n  );\n}\n\nfunction buildMongoSort(sort: SortDescriptor | undefined) {\n  if (sort === undefined || isEmpty(sort)) {\n    return undefined;\n  }\n\n  return mapValues(sort, (direction: SortDirection) =>\n    direction.toLowerCase() === 'desc' ? -1 : 1\n  );\n}\n\nasync function debugCall<Result>(\n  func: () => Promise<Result>,\n  message: string,\n  ...params: unknown[]\n): Promise<Result> {\n  let result;\n  let error;\n\n  try {\n    result = await func();\n  } catch (err) {\n    error = err;\n  }\n\n  if (error !== undefined) {\n    debug(`${message} => Error`, ...params);\n\n    throw error;\n  }\n\n  debug(`${message} => %o`, ...params, result);\n\n  return result as Result;\n}\n\nconst findOneBatcher = Symbol('batcher');\n\ninterface FindOneOperation extends Operation {\n  params: [filter: Filter<any>, options: FindOptions<any>];\n}\n\nfunction batchableFindOne(\n  collection: Collection & {[findOneBatcher]?: Microbatcher<FindOneOperation>},\n  query: Filter<any>,\n  options: FindOptions<any>\n) {\n  assertIsObjectLike(query);\n\n  if (collection[findOneBatcher] === undefined) {\n    collection[findOneBatcher] = new Microbatcher(function (operations) {\n      const operationGroups = groupBy(operations, ({params: [query, options]}) => {\n        if (\n          hasOwnProperty(query, MONGODB_PRIMARY_IDENTIFIER_ATTRIBUTE_NAME) &&\n          Object.keys(query).length === 1\n        ) {\n          // 'query' has a single '_id' attribute\n          query = {[MONGODB_PRIMARY_IDENTIFIER_ATTRIBUTE_NAME]: '___???___'};\n        }\n\n        return JSON.stringify([query, options]);\n      });\n\n      for (const operations of Object.values(operationGroups)) {\n        if (operations.length > 1) {\n          // Multiple `findOne()` that can be transformed into a single `find()`\n\n          const ids = operations.map(\n            (operation) => operation.params[0][MONGODB_PRIMARY_IDENTIFIER_ATTRIBUTE_NAME]\n          );\n          const options = operations[0].params[1]; // All 'options' objects should be identical\n\n          collection\n            .find({[MONGODB_PRIMARY_IDENTIFIER_ATTRIBUTE_NAME]: {$in: ids}}, options)\n            .toArray()\n            .then(\n              (documents) => {\n                for (const {\n                  params: [query],\n                  resolve\n                } of operations) {\n                  const document = documents.find(\n                    (document) =>\n                      document[MONGODB_PRIMARY_IDENTIFIER_ATTRIBUTE_NAME] ===\n                      query[MONGODB_PRIMARY_IDENTIFIER_ATTRIBUTE_NAME]\n                  );\n                  resolve(document !== undefined ? document : null);\n                }\n              },\n              (error) => {\n                for (const {reject} of operations) {\n                  reject(error);\n                }\n              }\n            );\n        } else {\n          // Single `findOne()`\n\n          const {\n            params: [query, options],\n            resolve,\n            reject\n          } = operations[0];\n\n          collection.findOne(query, options).then(resolve, reject);\n        }\n      }\n    });\n  }\n\n  return collection[findOneBatcher]!.batch(query, options);\n}\n"
  },
  {
    "path": "packages/mongodb-store/tsconfig.json",
    "content": "{\n  \"extends\": \"@mvila/tsconfig\",\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/navigator/README.md",
    "content": "# @layr/navigator\n\nA base class for implementing Layr navigators.\n\n## Installation\n\n```\nnpm install @layr/navigator\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/navigator/package.json",
    "content": "{\n  \"name\": \"@layr/navigator\",\n  \"version\": \"2.0.55\",\n  \"description\": \"A base class for implementing Layr navigators\",\n  \"keywords\": [\n    \"layr\",\n    \"navigator\",\n    \"navigation\",\n    \"base\",\n    \"class\"\n  ],\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/layrjs/layr/tree/master/packages/navigator\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"dist/node-cjs/index.js\",\n  \"module\": \"dist/node-esm/index.js\",\n  \"engines\": {\n    \"node\": \">=16.0.0\"\n  },\n  \"scripts\": {\n    \"build\": \"dev-tools build:ts-library\",\n    \"prepare\": \"npm run build\",\n    \"test\": \"dev-tools test:ts-library\",\n    \"publish:package\": \"dev-tools publish:package\",\n    \"update\": \"dev-tools update:dependencies\"\n  },\n  \"dependencies\": {\n    \"@layr/component\": \"^2.0.51\",\n    \"@layr/observable\": \"^1.0.16\",\n    \"core-helpers\": \"^1.0.8\",\n    \"possibly-async\": \"^1.0.7\",\n    \"qs\": \"^6.11.0\",\n    \"tslib\": \"^2.4.1\"\n  },\n  \"devDependencies\": {\n    \"@mvila/dev-tools\": \"^1.3.1\",\n    \"@mvila/tsconfig\": \"^1.0.6\",\n    \"@types/jest\": \"^29.2.5\",\n    \"@types/qs\": \"^6.9.7\"\n  }\n}\n"
  },
  {
    "path": "packages/navigator/src/index.ts",
    "content": "export * from './navigator';\nexport * from './utilities';\n"
  },
  {
    "path": "packages/navigator/src/navigator.ts",
    "content": "import {Observable} from '@layr/observable';\nimport {assertNoUnknownOptions} from 'core-helpers';\nimport {possiblyAsync} from 'possibly-async';\n\nimport {isNavigatorInstance, normalizeURL, stringifyURL, parseQuery} from './utilities';\n\ndeclare global {\n  interface Function {\n    matchURL: (url: URL | string) => {identifiers: any; params: any} | undefined;\n    generateURL: (params?: any, options?: URLOptions) => string;\n    generatePath: () => string;\n    generateQueryString: (params?: any) => string;\n    navigate: (params?: any, options?: URLOptions & NavigationOptions) => Promise<void> | undefined;\n    redirect: (params?: any, options?: URLOptions & NavigationOptions) => Promise<void> | undefined;\n    reload: (params?: any, options?: URLOptions) => void;\n    isActive: () => boolean;\n    Link: (props: {params?: any; hash?: string; [key: string]: any}) => any;\n  }\n}\n\nexport type URLOptions = {hash?: string};\n\nexport type NavigationOptions = {silent?: boolean; defer?: boolean};\n\ntype NavigatorPlugin = (navigator: Navigator) => void;\n\ntype AddressableMethodWrapper = (receiver: any, method: Function, params: any) => any;\n\ntype CustomRouteDecorator = (method: Function) => void;\n\nexport type NavigatorOptions = {\n  plugins?: NavigatorPlugin[];\n};\n\n/**\n * *Inherits from [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class).*\n *\n * An abstract class from which classes such as [`BrowserNavigator`](https://layrjs.com/docs/v2/reference/browser-navigator) or [`MemoryNavigator`](https://layrjs.com/docs/v2/reference/memory-navigator) are constructed. Unless you build a custom navigator, you probably won't have to use this class directly.\n */\nexport abstract class Navigator extends Observable(Object) {\n  constructor(options: NavigatorOptions = {}) {\n    super();\n\n    const {plugins, ...otherOptions} = options;\n\n    assertNoUnknownOptions(otherOptions);\n\n    if (plugins !== undefined) {\n      this.applyPlugins(plugins);\n    }\n\n    this.mount();\n  }\n\n  mount() {\n    // Override this method to implement custom mount logic\n  }\n\n  unmount() {\n    // Override this method to implement custom unmount logic\n  }\n\n  // === Current Location ===\n\n  /**\n   * Returns the current URL of the navigator.\n   *\n   * @returns A string.\n   *\n   * @example\n   * ```\n   * // See the definition of `navigator` in the `findRouteByURL()` example\n   *\n   * navigator.navigate('/movies/inception?showDetails=1#actors');\n   * navigator.getCurrentURL(); // => /movies/inception?showDetails=1#actors'\n   * ```\n   *\n   * @category Current Location\n   */\n  getCurrentURL() {\n    return stringifyURL(this._getCurrentURL());\n  }\n\n  abstract _getCurrentURL(): URL;\n\n  /**\n   * Returns the path of the current URL.\n   *\n   * @returns A string.\n   *\n   * @example\n   * ```\n   * // See the definition of `navigator` in the `findRouteByURL()` example\n   *\n   * navigator.navigate('/movies/inception?showDetails=1#actors');\n   * navigator.getCurrentPath(); // => '/movies/inception'\n   * ```\n   *\n   * @category Current Location\n   */\n  getCurrentPath() {\n    return this._getCurrentURL().pathname;\n  }\n\n  /**\n   * Returns an object representing the query of the current URL.\n   *\n   * The [`qs`](https://github.com/ljharb/qs) package is used under the hood to parse the query.\n   *\n   * @returns A plain object.\n   *\n   * @example\n   * ```\n   * // See the definition of `navigator` in the `findRouteByURL()` example\n   *\n   * navigator.navigate('/movies/inception?showDetails=1#actors');\n   * navigator.getCurrentQuery(); // => {showDetails: '1'}\n   * ```\n   *\n   * @category Current Location\n   */\n  getCurrentQuery<T extends object = object>() {\n    return parseQuery<T>(this._getCurrentURL().search);\n  }\n\n  /**\n   * Returns the hash (i.e., the [fragment identifier](https://en.wikipedia.org/wiki/URI_fragment)) contained in the current URL. If the current URL doesn't contain a hash, returns `undefined`.\n   *\n   * @returns A string or `undefined`.\n   *\n   * @example\n   * ```\n   * // See the definition of `navigator` in the `findRouteByURL()` example\n   *\n   * navigator.navigate('/movies/inception?showDetails=1#actors');\n   * navigator.getCurrentHash(); // => 'actors'\n   *\n   * navigator.navigate('/movies/inception?showDetails=1#actors');\n   * navigator.getCurrentHash(); // => 'actors'\n   *\n   * navigator.navigate('/movies/inception?showDetails=1#');\n   * navigator.getCurrentHash(); // => undefined\n   *\n   * navigator.navigate('/movies/inception?showDetails=1');\n   * navigator.getCurrentHash(); // => undefined\n   * ```\n   *\n   * @category Current Location\n   */\n  getCurrentHash() {\n    let hash = this._getCurrentURL().hash;\n\n    if (hash.startsWith('#')) {\n      hash = hash.slice(1);\n    }\n\n    if (hash === '') {\n      return undefined;\n    }\n\n    return hash;\n  }\n\n  // === Navigation ===\n\n  /**\n   * Navigates to a URL.\n   *\n   * The specified URL is added to the navigator's history.\n   *\n   * The observers of the navigator are automatically called.\n   *\n   * Note that instead of using this method, you can use the handy `navigate()` shortcut function that you get when you define a route with the [`@route()`](https://layrjs.com/docs/v2/reference/routable#route-decorator) decorator.\n   *\n   * @param url A string or a [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) object.\n   * @param [options.silent] A boolean specifying whether the navigator's observers should *not* be called (default: `false`).\n   * @param [options.defer] A boolean specifying whether the calling of the navigator's observers should be deferred to the next tick (default: `true`).\n   *\n   * @example\n   * ```\n   * navigator.navigate('/movies/inception');\n   *\n   * // Same as above, but in a more idiomatic way:\n   * Movie.Viewer.navigate({slug: 'inception});\n   * ```\n   *\n   * @category Navigation\n   * @possiblyasync\n   */\n  navigate(url: string | URL, options: NavigationOptions = {}) {\n    const {silent = false, defer = true} = options;\n\n    this._navigate(normalizeURL(url));\n\n    if (silent) {\n      return;\n    }\n\n    return possiblyDeferred(defer, () => {\n      this.callObservers();\n    });\n  }\n\n  abstract _navigate(url: URL): void;\n\n  /**\n   * Redirects to a URL.\n   *\n   * The specified URL replaces the current entry of the navigator's history.\n   *\n   * The observers of the navigator are automatically called.\n   *\n   * Note that instead of using this method, you can use the handy `redirect()` shortcut function that you get when you define a route with the [`@route()`](https://layrjs.com/docs/v2/reference/routable#route-decorator) decorator.\n   *\n   * @param url A string or a [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) object.\n   * @param [options.silent] A boolean specifying whether the navigator's observers should *not* be called (default: `false`).\n   * @param [options.defer] A boolean specifying whether the calling of the navigator's observers should be deferred to the next tick (default: `true`).\n   *\n   * @example\n   * ```\n   * navigator.redirect('/sign-in');\n   *\n   * // Same as above, but in a more idiomatic way:\n   * Session.SignIn.redirect();\n   * ```\n   *\n   * @category Navigation\n   * @possiblyasync\n   */\n  redirect(url: string | URL, options: NavigationOptions = {}) {\n    const {silent = false, defer = true} = options;\n\n    this._redirect(normalizeURL(url));\n\n    if (silent) {\n      return;\n    }\n\n    return possiblyDeferred(defer, () => {\n      this.callObservers();\n    });\n  }\n\n  abstract _redirect(url: URL): void;\n\n  /**\n   * Reloads the execution environment with the specified URL.\n   *\n   * Note that instead of using this method, you can use the handy `reload()` shortcut function that you get when you define a route with the [`@route()`](https://layrjs.com/docs/v2/reference/routable#route-decorator) decorator.\n   *\n   * @param url A string or a [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) object.\n   *\n   * @example\n   * ```\n   * navigator.reload('/');\n   *\n   * // Same as above, but in a more idiomatic way:\n   * Frontend.Home.reload();\n   * ```\n   *\n   * @category Navigation\n   */\n  reload(url?: string | URL) {\n    const normalizedURL = url !== undefined ? normalizeURL(url) : undefined;\n\n    this._reload(normalizedURL);\n  }\n\n  abstract _reload(url: URL | undefined): void;\n\n  /**\n   * Move forwards or backwards through the navigator's history.\n   *\n   * The observers of the navigator are automatically called.\n   *\n   * @param delta A number representing the position in the navigator's history to which you want to move, relative to the current entry. A negative value moves backwards, a positive value moves forwards.\n   * @param [options.silent] A boolean specifying whether the navigator's observers should *not* be called (default: `false`).\n   * @param [options.defer] A boolean specifying whether the calling of the navigator's observers should be deferred to the next tick (default: `true`).\n   *\n   * @example\n   * ```\n   * navigator.go(-2); // Move backwards by two entries of the navigator's history\n   *\n   * navigator.go(-1); // Equivalent of calling `navigator.goBack()`\n   *\n   * navigator.go(1); // Equivalent of calling `navigator.goForward()`\n   *\n   * navigator.go(2); // Move forward two entries of the navigator's history\n   * ```\n   *\n   * @category Navigation\n   * @possiblyasync\n   */\n  go(delta: number, options: NavigationOptions = {}) {\n    const {silent = false, defer = true} = options;\n\n    return possiblyAsync(this._go(delta), () => {\n      if (silent) {\n        return;\n      }\n\n      return possiblyDeferred(defer, () => {\n        this.callObservers();\n      });\n    });\n  }\n\n  abstract _go(delta: number): void;\n\n  /**\n   * Go back to the previous entry in the navigator's history.\n   *\n   * This method is the equivalent of calling `navigator.go(-1)`.\n   *\n   * The observers of the navigator are automatically called.\n   *\n   * @param [options.silent] A boolean specifying whether the navigator's observers should *not* be called (default: `false`).\n   * @param [options.defer] A boolean specifying whether the calling of the navigator's observers should be deferred to the next tick (default: `true`).\n   *\n   * @category Navigation\n   * @possiblyasync\n   */\n  goBack(options: NavigationOptions = {}) {\n    return this.go(-1, options);\n  }\n\n  /**\n   * Go back to the first entry in the navigator's history.\n   *\n   * The observers of the navigator are automatically called.\n   *\n   * @param [options.silent] A boolean specifying whether the navigator's observers should *not* be called (default: `false`).\n   * @param [options.defer] A boolean specifying whether the calling of the navigator's observers should be deferred to the next tick (default: `true`).\n   *\n   * @category Navigation\n   * @possiblyasync\n   */\n  goBackToRoot(options: NavigationOptions = {}) {\n    const index = this.getHistoryIndex();\n\n    if (index < 1) {\n      return undefined;\n    }\n\n    return this.go(-index, options);\n  }\n\n  /**\n   * Go forward to the next entry in the navigator's history.\n   *\n   * This method is the equivalent of calling `navigator.go(1)`.\n   *\n   * The observers of the navigator are automatically called.\n   *\n   * @param [options.silent] A boolean specifying whether the navigator's observers should *not* be called (default: `false`).\n   * @param [options.defer] A boolean specifying whether the calling of the navigator's observers should be deferred to the next tick (default: `true`).\n   *\n   * @category Navigation\n   * @possiblyasync\n   */\n  goForward(options: NavigationOptions = {}) {\n    return this.go(1, options);\n  }\n\n  /**\n   * Returns the number of entries in the navigator's history.\n   *\n   * @category Navigation\n   */\n  getHistoryLength() {\n    return this._getHistoryLength();\n  }\n\n  abstract _getHistoryLength(): number;\n\n  /**\n   * Returns the current index in the navigator's history.\n   *\n   * @category Navigation\n   */\n  getHistoryIndex() {\n    return this._getHistoryIndex();\n  }\n\n  abstract _getHistoryIndex(): number;\n\n  // === Observability ===\n\n  /**\n   * See the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) class.\n   *\n   * @category Observability\n   */\n\n  // === Customization ===\n\n  applyPlugins(plugins: NavigatorPlugin[]) {\n    for (const plugin of plugins) {\n      plugin(this);\n    }\n  }\n\n  _addressableMethodWrappers: AddressableMethodWrapper[] = [];\n\n  addAddressableMethodWrapper(methodWrapper: AddressableMethodWrapper) {\n    // TODO: Support multiple addressable method wrappers\n\n    if (this._addressableMethodWrappers.length > 0) {\n      throw new Error('You cannot add more than one addressable method wrapper');\n    }\n\n    this._addressableMethodWrappers.push(methodWrapper);\n  }\n\n  callAddressableMethodWrapper(receiver: any, method: Function, params: any) {\n    // TODO: Support multiple addressable method wrappers\n\n    if (this._addressableMethodWrappers.length === 1) {\n      const methodWrapper = this._addressableMethodWrappers[0];\n      return methodWrapper(receiver, method, params);\n    } else {\n      return method.call(receiver, params);\n    }\n  }\n\n  _customRouteDecorators: CustomRouteDecorator[] = [];\n\n  addCustomRouteDecorator(decorator: CustomRouteDecorator) {\n    this._customRouteDecorators.push(decorator);\n  }\n\n  applyCustomRouteDecorators(routable: any, method: Function) {\n    for (const customRouteDecorator of this._customRouteDecorators) {\n      customRouteDecorator.call(routable, method);\n    }\n  }\n\n  Link!: (props: any) => any;\n\n  // === Utilities ===\n\n  static isNavigator(value: any): value is Navigator {\n    return isNavigatorInstance(value);\n  }\n}\n\nfunction possiblyDeferred(defer: boolean, func: Function) {\n  if (!defer) {\n    func();\n    return;\n  }\n\n  return new Promise<void>((resolve, reject) => {\n    setTimeout(() => {\n      try {\n        func();\n      } catch (error) {\n        reject(error);\n        return;\n      }\n\n      resolve();\n    }, 0);\n  });\n}\n"
  },
  {
    "path": "packages/navigator/src/utilities.test.ts",
    "content": "import {normalizeURL, stringifyURL, parseQuery, stringifyQuery} from './utilities';\n\ndescribe('Utilities', () => {\n  test('normalizeURL() and stringifyURL()', async () => {\n    let url = 'https://username:password@domain.com:80/path?query=1#fragment';\n    let normalizedURL = normalizeURL(url);\n\n    expect(normalizedURL).toBeInstanceOf(URL);\n    expect(stringifyURL(normalizedURL)).toBe(url);\n    expect(normalizeURL(normalizedURL)).toBe(normalizedURL);\n\n    expect(stringifyURL(normalizeURL('/movies'))).toBe('/movies');\n    expect(stringifyURL(normalizeURL('movies'))).toBe('/movies');\n\n    expect(normalizeURL('https://localhost').pathname).toBe('/');\n    expect(normalizeURL('https://localhost/').pathname).toBe('/');\n    expect(normalizeURL('capacitor://localhost').pathname).toBe('/');\n    expect(normalizeURL('capacitor://localhost/').pathname).toBe('/');\n\n    // @ts-expect-error\n    expect(() => normalizeURL(123)).toThrow(\n      \"Expected a string or a URL instance, but received a value of type 'number'\"\n    );\n\n    expect(() => normalizeURL('https://?')).toThrow(\n      \"The specified URL is invalid (URL: 'https://?')\"\n    );\n  });\n\n  test('parseQuery() and stringifyQuery()', async () => {\n    expect(parseQuery('')).toEqual({});\n    expect(parseQuery('?')).toEqual({});\n    expect(parseQuery('category=drama&sortBy=title')).toEqual({category: 'drama', sortBy: 'title'});\n    expect(parseQuery('?category=drama&sortBy=title')).toEqual({\n      category: 'drama',\n      sortBy: 'title'\n    });\n\n    expect(stringifyQuery(undefined)).toEqual('');\n    expect(stringifyQuery({})).toEqual('');\n    expect(stringifyQuery({category: 'drama', sortBy: 'title'})).toEqual(\n      'category=drama&sortBy=title'\n    );\n  });\n});\n"
  },
  {
    "path": "packages/navigator/src/utilities.ts",
    "content": "import qs from 'qs';\nimport {getTypeOf} from 'core-helpers';\n\nimport type {Navigator} from './navigator';\n\nconst INTERNAL_LAYR_BASE_URL = 'http://internal.layr';\n\n/**\n * Returns whether the specified value is a navigator class.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isNavigatorClass(value: any): value is typeof Navigator {\n  return typeof value?.isNavigator === 'function';\n}\n\n/**\n * Returns whether the specified value is a navigator instance.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isNavigatorInstance(value: any): value is Navigator {\n  return typeof value?.constructor?.isNavigator === 'function';\n}\n\nexport function assertIsNavigatorInstance(value: any): asserts value is Navigator {\n  if (!isNavigatorInstance(value)) {\n    throw new Error(\n      `Expected a navigator instance, but received a value of type '${getTypeOf(value)}'`\n    );\n  }\n}\n\nexport function normalizeURL(url: URL | string) {\n  if (url instanceof URL) {\n    return fixCapacitorURL(url);\n  }\n\n  if (typeof url !== 'string') {\n    throw new Error(\n      `Expected a string or a URL instance, but received a value of type '${getTypeOf(url)}'`\n    );\n  }\n\n  try {\n    return fixCapacitorURL(new URL(url, `${INTERNAL_LAYR_BASE_URL}/`));\n  } catch (error) {\n    throw new Error(`The specified URL is invalid (URL: '${url}')`);\n  }\n}\n\nfunction fixCapacitorURL(url: URL) {\n  if (url.protocol === 'capacitor:' && url.pathname === '') {\n    // Fix an issue where to root URL of a Capacitor app is 'capacitor://localhost'\n    // and `pathname` is an empty string\n    url = new URL(url.toString());\n    url.pathname = '/';\n  }\n\n  return url;\n}\n\nexport function stringifyURL(url: URL) {\n  if (!(url instanceof URL)) {\n    throw new Error(`Expected a URL instance, but received a value of type '${getTypeOf(url)}'`);\n  }\n\n  let urlString = url.toString();\n\n  if (urlString.startsWith(INTERNAL_LAYR_BASE_URL)) {\n    urlString = urlString.slice(INTERNAL_LAYR_BASE_URL.length);\n  }\n\n  return urlString;\n}\n\nexport function parseQuery<T extends object = object>(queryString: string) {\n  if (queryString.startsWith('?')) {\n    queryString = queryString.slice(1);\n  }\n\n  return qs.parse(queryString) as T;\n}\n\nexport function stringifyQuery(query: object | undefined) {\n  return qs.stringify(query);\n}\n"
  },
  {
    "path": "packages/navigator/tsconfig.json",
    "content": "{\n  \"extends\": \"@mvila/tsconfig\",\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/observable/README.md",
    "content": "# @layr/observable\n\nObserve JavaScript objects, arrays, or your own classes.\n\n## Installation\n\n```\nnpm install @layr/observable\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/observable/package.json",
    "content": "{\n  \"name\": \"@layr/observable\",\n  \"version\": \"1.0.16\",\n  \"description\": \"Observe JavaScript objects, arrays, or your own classes\",\n  \"keywords\": [\n    \"observe\",\n    \"observer\",\n    \"notify\",\n    \"changes\",\n    \"object\",\n    \"array\",\n    \"class\"\n  ],\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/layrjs/layr/tree/master/packages/observable\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"dist/node-cjs/index.js\",\n  \"module\": \"dist/node-esm/index.js\",\n  \"engines\": {\n    \"node\": \">=16.0.0\"\n  },\n  \"scripts\": {\n    \"build\": \"dev-tools build:ts-library\",\n    \"prepare\": \"npm run build\",\n    \"test\": \"dev-tools test:ts-library\",\n    \"publish:package\": \"dev-tools publish:package\",\n    \"update\": \"dev-tools update:dependencies\"\n  },\n  \"dependencies\": {\n    \"core-helpers\": \"^1.0.8\",\n    \"tslib\": \"^2.4.1\"\n  },\n  \"devDependencies\": {\n    \"@mvila/dev-tools\": \"^1.3.1\",\n    \"@mvila/tsconfig\": \"^1.0.6\",\n    \"@types/jest\": \"^29.2.5\"\n  }\n}\n"
  },
  {
    "path": "packages/observable/src/index.ts",
    "content": "export * from './observable';\n"
  },
  {
    "path": "packages/observable/src/observable.test.ts",
    "content": "import {\n  Observable,\n  createObservable,\n  isObservable,\n  canBeObserved,\n  isEmbeddable,\n  ObservableType\n} from './observable';\n\ndescribe('Observable', () => {\n  describe('Observable array', () => {\n    let originalArray: any[];\n    let observableArray: any[] & ObservableType;\n\n    beforeEach(() => {\n      originalArray = [3, 2, 1];\n      observableArray = createObservable(originalArray);\n    });\n\n    it('Should be an array', () => {\n      expect(Array.isArray(originalArray)).toBe(true);\n      expect(Array.isArray(observableArray)).toBe(true);\n    });\n\n    it('Should be an observable', () => {\n      expect(isObservable(originalArray)).toBe(false);\n      expect(isObservable(observableArray)).toBe(true);\n    });\n\n    it('Should be embeddable', () => {\n      expect(isEmbeddable(observableArray)).toBe(true);\n    });\n\n    it('Should be usable as the target of a new observable', () => {\n      const newObservableArray = createObservable(observableArray);\n      expect(isObservable(newObservableArray)).toBe(true);\n    });\n\n    it('Should be equal to the original array', () => {\n      expect(observableArray).toEqual(originalArray);\n    });\n\n    it('Should produce the same JSON as the original array', () => {\n      expect(JSON.stringify(observableArray)).toBe(JSON.stringify(originalArray));\n    });\n\n    it('Should call observers when callObservers() is called', () => {\n      const observer = jest.fn();\n      observableArray.addObserver(observer);\n      expect(observer).not.toHaveBeenCalled();\n      observableArray.callObservers();\n      expect(observer).toHaveBeenCalled();\n    });\n\n    it('Should call observers when changing an item', () => {\n      const observer = jest.fn();\n      observableArray.addObserver(observer);\n      expect(observer).not.toHaveBeenCalled();\n      observableArray[0] = 1;\n      expect(observer).toHaveBeenCalled();\n    });\n\n    it('Should not call observers when setting an item with the same value', () => {\n      const observer = jest.fn();\n      observableArray.addObserver(observer);\n      expect(observer).not.toHaveBeenCalled();\n      observableArray[0] = 3;\n      expect(observer).not.toHaveBeenCalled();\n    });\n\n    it('Should call observers when changing array length', () => {\n      const observer = jest.fn();\n      observableArray.addObserver(observer);\n      expect(observer).not.toHaveBeenCalled();\n      observableArray.length = 0;\n      expect(observer).toHaveBeenCalled();\n    });\n\n    it(`Shouldn't call removed observers`, () => {\n      const observer1 = jest.fn();\n      observableArray.addObserver(observer1);\n\n      const observer2 = jest.fn();\n      observableArray.addObserver(observer2);\n\n      observableArray[0] = 4;\n\n      const numberOfCalls1 = observer1.mock.calls.length;\n      const numberOfCalls2 = observer2.mock.calls.length;\n\n      expect(numberOfCalls1).not.toBe(0);\n      expect(numberOfCalls2).not.toBe(0);\n\n      observableArray.removeObserver(observer1);\n\n      observableArray[0] = 5;\n\n      expect(observer1.mock.calls.length).toBe(numberOfCalls1);\n      expect(observer2.mock.calls.length).not.toBe(numberOfCalls1);\n    });\n\n    describe('Observable item', () => {\n      it('Should call observers when updating an observable item', () => {\n        const observer = jest.fn();\n        observableArray.addObserver(observer);\n\n        expect(observer).toHaveBeenCalledTimes(0);\n\n        const observableItem = createObservable([] as any[]);\n        observableArray[0] = observableItem;\n\n        expect(observer).toHaveBeenCalledTimes(1);\n\n        observableItem[0] = 1;\n\n        expect(observer).toHaveBeenCalledTimes(2);\n\n        const observableItem2 = createObservable([] as any[]);\n        observableArray.push(observableItem2);\n\n        expect(observer).toHaveBeenCalledTimes(3);\n\n        observableItem2[0] = 1;\n\n        expect(observer).toHaveBeenCalledTimes(4);\n      });\n\n      it(`Should stop calling observers when an observable item has been removed`, () => {\n        const observer = jest.fn();\n        observableArray.addObserver(observer);\n\n        expect(observer).toHaveBeenCalledTimes(0);\n\n        const observableItem = createObservable([] as any[]);\n        observableArray[3] = observableItem;\n\n        expect(observer).toHaveBeenCalledTimes(1);\n\n        observableItem[0] = 1;\n\n        expect(observer).toHaveBeenCalledTimes(2);\n\n        observableArray[3] = undefined;\n\n        expect(observer).toHaveBeenCalledTimes(3);\n\n        observableItem[0] = 2;\n\n        expect(observer).toHaveBeenCalledTimes(3);\n\n        observableArray[3] = observableItem;\n\n        expect(observer).toHaveBeenCalledTimes(4);\n\n        observableItem[0] = 3;\n\n        expect(observer).toHaveBeenCalledTimes(5);\n\n        observableArray.pop();\n\n        expect(observer).toHaveBeenCalledTimes(7);\n\n        observableItem[0] = 4;\n\n        expect(observer).toHaveBeenCalledTimes(7);\n      });\n\n      it('Should observe existing items', () => {\n        const observableArray = createObservable([createObservable([1])]);\n        const observer = jest.fn();\n        observableArray.addObserver(observer);\n\n        expect(observer).toHaveBeenCalledTimes(0);\n\n        observableArray[0][0] = 2;\n\n        expect(observer).toHaveBeenCalledTimes(1);\n      });\n\n      it('Should make existing items observable when possible', () => {\n        const observableArray = createObservable([[1]]);\n\n        expect(isObservable(observableArray[0])).toBe(true);\n\n        const observer = jest.fn();\n        observableArray.addObserver(observer);\n\n        expect(observer).toHaveBeenCalledTimes(0);\n\n        observableArray[0][0] = 2;\n\n        expect(observer).toHaveBeenCalledTimes(1);\n\n        observableArray[0] = [3];\n\n        expect(isObservable(observableArray[0])).toBe(true);\n\n        expect(observer).toHaveBeenCalledTimes(2);\n\n        observableArray[0][0] = 4;\n\n        expect(observer).toHaveBeenCalledTimes(3);\n      });\n    });\n\n    describe('Mutator methods', () => {\n      it('Should call observers when using copyWithin()', () => {\n        const observer = jest.fn();\n        observableArray.addObserver(observer);\n        expect(observer).not.toHaveBeenCalled();\n        observableArray.copyWithin(0, 1);\n        expect(observer).toHaveBeenCalled();\n      });\n\n      it('Should call observers when using fill()', () => {\n        const observer = jest.fn();\n        observableArray.addObserver(observer);\n        expect(observer).not.toHaveBeenCalled();\n        observableArray.fill(0);\n        expect(observer).toHaveBeenCalled();\n      });\n\n      it('Should call observers when using pop()', () => {\n        const observer = jest.fn();\n        observableArray.addObserver(observer);\n        expect(observer).not.toHaveBeenCalled();\n        observableArray.pop();\n        expect(observer).toHaveBeenCalled();\n      });\n\n      it('Should call observers when using push()', () => {\n        const observer = jest.fn();\n        observableArray.addObserver(observer);\n        expect(observer).not.toHaveBeenCalled();\n        observableArray.push(4);\n        expect(observer).toHaveBeenCalled();\n      });\n\n      it('Should call observers when using reverse()', () => {\n        const observer = jest.fn();\n        observableArray.addObserver(observer);\n        expect(observer).not.toHaveBeenCalled();\n        observableArray.reverse();\n        expect(observer).toHaveBeenCalled();\n      });\n\n      it('Should call observers when using shift()', () => {\n        const observer = jest.fn();\n        observableArray.addObserver(observer);\n        expect(observer).not.toHaveBeenCalled();\n        observableArray.shift();\n        expect(observer).toHaveBeenCalled();\n      });\n\n      it('Should call observers when using sort()', () => {\n        const observer = jest.fn();\n        observableArray.addObserver(observer);\n        expect(observer).not.toHaveBeenCalled();\n        observableArray.sort();\n        expect(observer).toHaveBeenCalled();\n      });\n\n      it('Should call observers when using splice()', () => {\n        const observer = jest.fn();\n        observableArray.addObserver(observer);\n        expect(observer).not.toHaveBeenCalled();\n        observableArray.splice(0, 1);\n        expect(observer).toHaveBeenCalled();\n      });\n\n      it('Should call observers when using unshift()', () => {\n        const observer = jest.fn();\n        observableArray.addObserver(observer);\n        expect(observer).not.toHaveBeenCalled();\n        observableArray.unshift(4);\n        expect(observer).toHaveBeenCalled();\n      });\n    });\n  });\n\n  describe('Observable object', () => {\n    let originalObject: {[key: string]: any};\n    let observableObject: {[key: string]: any} & ObservableType;\n\n    beforeEach(() => {\n      originalObject = {\n        id: 1\n      };\n      observableObject = createObservable(originalObject);\n    });\n\n    it('Should be an object', () => {\n      expect(typeof originalObject).toEqual('object');\n      expect(typeof observableObject).toEqual('object');\n    });\n\n    it('Should be an observable', () => {\n      expect(isObservable(originalObject)).toBe(false);\n      expect(isObservable(observableObject)).toBe(true);\n    });\n\n    it('Should be embeddable', () => {\n      expect(isEmbeddable(observableObject)).toBe(true);\n    });\n\n    it('Should be usable as the target of a new observable', () => {\n      const newCustomObservable = createObservable(observableObject);\n      expect(isObservable(newCustomObservable)).toBe(true);\n    });\n\n    it('Should be equal to the original object', () => {\n      expect(observableObject).toEqual(originalObject);\n    });\n\n    it('Should produce the same JSON as the original object', () => {\n      expect(JSON.stringify(observableObject)).toBe(JSON.stringify(originalObject));\n    });\n\n    it('Should call observers when callObservers() is called', () => {\n      const observer = jest.fn();\n      observableObject.addObserver(observer);\n      expect(observer).not.toHaveBeenCalled();\n      observableObject.callObservers();\n      expect(observer).toHaveBeenCalled();\n    });\n\n    it('Should call observers when changing an attribute', () => {\n      const observer = jest.fn();\n      observableObject.addObserver(observer);\n      expect(observer).not.toHaveBeenCalled();\n      observableObject.id = 2;\n      expect(observer).toHaveBeenCalled();\n    });\n\n    it('Should not call observers when setting an attribute with the same value', () => {\n      const observer = jest.fn();\n      observableObject.addObserver(observer);\n      expect(observer).not.toHaveBeenCalled();\n      observableObject.id = 1;\n      expect(observer).not.toHaveBeenCalled();\n    });\n\n    it(`Shouldn't call removed observers`, () => {\n      const observer1 = jest.fn();\n      observableObject.addObserver(observer1);\n\n      const observer2 = jest.fn();\n      observableObject.addObserver(observer2);\n\n      observableObject.id = 2;\n\n      const numberOfCalls1 = observer1.mock.calls.length;\n      const numberOfCalls2 = observer2.mock.calls.length;\n\n      expect(numberOfCalls1).not.toBe(0);\n      expect(numberOfCalls2).not.toBe(0);\n\n      observableObject.removeObserver(observer1);\n\n      observableObject.id = 3;\n\n      expect(observer1.mock.calls.length).toBe(numberOfCalls1);\n      expect(observer2.mock.calls.length).not.toBe(numberOfCalls1);\n    });\n\n    describe('Observable attribute', () => {\n      it('Should call observers when updating an observable attribute', () => {\n        const observableAttribute = createObservable({id: 1});\n        observableObject.attribute = observableAttribute;\n        const observer = jest.fn();\n        observableObject.addObserver(observer);\n        expect(observer).not.toHaveBeenCalled();\n        observableAttribute.id = 2;\n        expect(observer).toHaveBeenCalled();\n      });\n\n      it(`Should stop calling observers when an observable attribute has been removed`, () => {\n        const observableAttribute = createObservable({} as any);\n        observableObject.attribute = observableAttribute;\n\n        const observer = jest.fn();\n        observableObject.addObserver(observer);\n\n        delete observableObject.attribute;\n\n        const numberOfCalls = observer.mock.calls.length;\n        expect(numberOfCalls).not.toBe(0);\n\n        observableAttribute.id = 2;\n        expect(observer.mock.calls.length).toBe(numberOfCalls);\n      });\n\n      it('Should observe existing attributes', () => {\n        const observableObject = createObservable({innerObject: createObservable({id: 1})});\n        const observer = jest.fn();\n        observableObject.addObserver(observer);\n\n        expect(observer).toHaveBeenCalledTimes(0);\n\n        observableObject.innerObject.id = 2;\n\n        expect(observer).toHaveBeenCalledTimes(1);\n      });\n\n      it('Should make existing attributes observable when possible', () => {\n        const observableObject = createObservable({innerObject: {id: 1}});\n\n        expect(isObservable(observableObject.innerObject)).toBe(true);\n\n        const observer = jest.fn();\n        observableObject.addObserver(observer);\n\n        expect(observer).toHaveBeenCalledTimes(0);\n\n        observableObject.innerObject.id = 2;\n\n        expect(observer).toHaveBeenCalledTimes(1);\n\n        observableObject.innerObject = {id: 3};\n\n        expect(isObservable(observableObject.innerObject)).toBe(true);\n\n        expect(observer).toHaveBeenCalledTimes(2);\n\n        observableObject.innerObject.id = 4;\n\n        expect(observer).toHaveBeenCalledTimes(3);\n      });\n    });\n\n    describe('Forked observable object', () => {\n      let observableObjectFork: {[key: string]: any} & ObservableType;\n\n      beforeEach(() => {\n        observableObjectFork = Object.create(observableObject);\n      });\n\n      it('Should not be an observable', () => {\n        expect(isObservable(observableObjectFork)).toBe(false);\n      });\n\n      it('Should not have a method such as addObserver()', () => {\n        expect(observableObjectFork.addObserver).toBeUndefined();\n        expect(observableObjectFork.removeObserver).toBeUndefined();\n        expect(observableObjectFork.callObservers).toBeUndefined();\n        expect(observableObjectFork.isObservable).toBeUndefined();\n      });\n\n      it('Should allow changing an attribute without changing the original observable', () => {\n        observableObjectFork.id = 2;\n        expect(observableObjectFork.id).toBe(2);\n        expect(observableObject.id).toBe(1);\n      });\n\n      it('Should not call the observers of the original observable when changing an attribute', () => {\n        const observer = jest.fn();\n        observableObject.addObserver(observer);\n        expect(observer).not.toHaveBeenCalled();\n        observableObjectFork.id = 2;\n        expect(observer).not.toHaveBeenCalled();\n      });\n\n      it('Should be able to become an observable', () => {\n        expect(isObservable(observableObjectFork)).toBe(false);\n        const observableObservableObjectFork = createObservable(observableObjectFork);\n        expect(isObservable(observableObservableObjectFork)).toBe(true);\n\n        const objectObserver = jest.fn();\n        observableObject.addObserver(objectObserver);\n        const objectForkObserver = jest.fn();\n        observableObservableObjectFork.addObserver(objectForkObserver);\n        expect(objectObserver).not.toHaveBeenCalled();\n        expect(objectForkObserver).not.toHaveBeenCalled();\n        observableObservableObjectFork.id = 2;\n        expect(objectForkObserver).toHaveBeenCalled();\n        expect(objectObserver).not.toHaveBeenCalled();\n      });\n    });\n  });\n\n  describe('Custom observable', () => {\n    class BaseCustomObservable extends Observable(Object) {\n      _id: number | undefined;\n\n      constructor({id}: {id?: number} = {}) {\n        super();\n        this._id = id;\n      }\n\n      static _limit: number;\n\n      static get limit() {\n        return this._limit;\n      }\n\n      static set limit(limit) {\n        this._limit = limit;\n        this.callObservers();\n      }\n\n      get id() {\n        return this._id;\n      }\n\n      set id(id) {\n        this._id = id;\n        this.callObservers();\n      }\n    }\n\n    describe('Observable class', () => {\n      let CustomObservable: typeof BaseCustomObservable;\n\n      beforeEach(() => {\n        CustomObservable = class CustomObservable extends BaseCustomObservable {};\n      });\n\n      it('Should be an observable', () => {\n        expect(isObservable(CustomObservable)).toBe(true);\n      });\n\n      it('Should be usable as the target of a new observable', () => {\n        const NewCustomObservable = createObservable(CustomObservable);\n        expect(isObservable(NewCustomObservable)).toBe(true);\n      });\n\n      it('Should call observers when callObservers() is called', () => {\n        const observer = jest.fn();\n        CustomObservable.addObserver(observer);\n        expect(observer).not.toHaveBeenCalled();\n        CustomObservable.callObservers();\n        expect(observer).toHaveBeenCalled();\n      });\n\n      it('Should call observers when changing an attribute', () => {\n        const observer = jest.fn();\n        CustomObservable.addObserver(observer);\n        expect(observer).not.toHaveBeenCalled();\n        CustomObservable.limit = 2;\n        expect(observer).toHaveBeenCalled();\n      });\n\n      it(`Shouldn't call removed observers`, () => {\n        const observer1 = jest.fn();\n        CustomObservable.addObserver(observer1);\n\n        const observer2 = jest.fn();\n        CustomObservable.addObserver(observer2);\n\n        CustomObservable.limit = 2;\n\n        const numberOfCalls1 = observer1.mock.calls.length;\n        const numberOfCalls2 = observer2.mock.calls.length;\n\n        expect(numberOfCalls1).not.toBe(0);\n        expect(numberOfCalls2).not.toBe(0);\n\n        CustomObservable.removeObserver(observer1);\n\n        CustomObservable.limit = 3;\n\n        expect(observer1.mock.calls.length).toBe(numberOfCalls1);\n        expect(observer2.mock.calls.length).not.toBe(numberOfCalls1);\n      });\n    });\n\n    describe('Observable instance', () => {\n      let customObservable: BaseCustomObservable;\n\n      beforeEach(() => {\n        customObservable = new BaseCustomObservable({id: 1});\n      });\n\n      it('Should be an observable', () => {\n        expect(isObservable(customObservable)).toBe(true);\n      });\n\n      it('Should be usable as the target of a new observable', () => {\n        const newCustomObservable = createObservable(customObservable);\n        expect(isObservable(newCustomObservable)).toBe(true);\n      });\n\n      it('Should call observers when callObservers() is called', () => {\n        const observer = jest.fn();\n        customObservable.addObserver(observer);\n        expect(observer).not.toHaveBeenCalled();\n        customObservable.callObservers();\n        expect(observer).toHaveBeenCalled();\n      });\n\n      it('Should call observers when changing an attribute', () => {\n        const observer = jest.fn();\n        customObservable.addObserver(observer);\n        expect(observer).not.toHaveBeenCalled();\n        customObservable.id = 2;\n        expect(observer).toHaveBeenCalled();\n      });\n\n      it(`Shouldn't call removed observers`, () => {\n        const observer1 = jest.fn();\n        customObservable.addObserver(observer1);\n\n        const observer2 = jest.fn();\n        customObservable.addObserver(observer2);\n\n        customObservable.id = 2;\n\n        const numberOfCalls1 = observer1.mock.calls.length;\n        const numberOfCalls2 = observer2.mock.calls.length;\n\n        expect(numberOfCalls1).not.toBe(0);\n        expect(numberOfCalls2).not.toBe(0);\n\n        customObservable.removeObserver(observer1);\n\n        customObservable.id = 3;\n\n        expect(observer1.mock.calls.length).toBe(numberOfCalls1);\n        expect(observer2.mock.calls.length).not.toBe(numberOfCalls1);\n      });\n    });\n  });\n\n  describe('Observable object referencing a non-embeddable object', () => {\n    let originalObject: {[key: string]: any};\n    let observableObject: {[key: string]: any} & ObservableType;\n\n    beforeEach(() => {\n      originalObject = {};\n      observableObject = createObservable(originalObject);\n    });\n\n    it(\"Shouldn't call referrer's observers\", () => {\n      class NonEmbeddable extends Observable(Object) {\n        static isEmbedded() {\n          return false;\n        }\n      }\n\n      const nonEmbeddable = new NonEmbeddable();\n\n      expect(isEmbeddable(nonEmbeddable)).toBe(false);\n\n      const observer = jest.fn();\n      observableObject.addObserver(observer);\n\n      expect(observer).toHaveBeenCalledTimes(0);\n\n      observableObject.nonEmbeddable = nonEmbeddable;\n\n      expect(observer).toHaveBeenCalledTimes(1);\n\n      nonEmbeddable.callObservers();\n\n      expect(observer).toHaveBeenCalledTimes(1);\n    });\n  });\n\n  describe('Observer payload', () => {\n    it('Should allow to specify a payload when calling observers', () => {\n      const observableObject = createObservable({} as any);\n      const observer = jest.fn();\n      observableObject.addObserver(observer);\n\n      expect(observer).not.toHaveBeenCalled();\n\n      observableObject.callObservers({source: 'server'});\n\n      expect(observer.mock.calls[0][0].source).toBe('server');\n    });\n  });\n\n  describe('Unobservable value', () => {\n    it('Should not be possible to observe a primitive', () => {\n      expect(canBeObserved(true)).toBe(false);\n\n      expect(canBeObserved(1)).toBe(false);\n\n      expect(canBeObserved('Hello')).toBe(false);\n\n      expect(canBeObserved(new Date())).toBe(false);\n\n      expect(canBeObserved(undefined)).toBe(false);\n\n      expect(canBeObserved(null)).toBe(false);\n\n      // @ts-expect-error\n      expect(() => createObservable('Hello')).toThrow(\n        'Cannot create an observable from a target that is not an object, an array, or a function'\n      );\n    });\n  });\n\n  describe('Observable with a circular reference', () => {\n    it('Should not loop indefinitely', () => {\n      const observableObject = createObservable({} as any);\n\n      const observer = jest.fn();\n      observableObject.addObserver(observer);\n      expect(observer).not.toHaveBeenCalled();\n      observableObject.circularReference = observableObject;\n      expect(observer).toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/observable/src/observable.ts",
    "content": "import {hasOwnProperty, Constructor, isClass, getTypeOf, isPlainObject} from 'core-helpers';\n\nexport type ObservableType = {\n  addObserver(observer: Observer): void;\n  removeObserver(observer: Observer): void;\n  callObservers(...args: any[]): void;\n  isObservable(value: any): boolean;\n};\n\nexport type Observer = ObserverFunction | ObservableType;\n\nexport type ObserverFunction = (...args: any[]) => void;\n\nexport type ObserverPayload = {[key: string]: unknown};\n\n/**\n * Brings observability to any class.\n *\n * This mixin is used to construct several Layr's classes such as [`Component`](https://layrjs.com/docs/v2/reference/component) or [`Attribute`](https://layrjs.com/docs/v2/reference/attribute). So, in most cases, you'll have the capabilities provided by this mixin without having to call it.\n *\n * #### Usage\n *\n * Call the `Observable()` mixin with any class to construct an [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) class. Then, you can add some observers by using the [`addObserver()`](https://layrjs.com/docs/v2/reference/observable#add-observer-dual-method) method, and trigger their execution anytime by using the [`callObservers()`](https://layrjs.com/docs/v2/reference/observable#call-observers-dual-method) method.\n *\n * For example, let's define a `Movie` class using the `Observable()` mixin:\n *\n * ```\n * // JS\n *\n * import {Observable} from '@layr/observable';\n *\n * class Movie extends Observable(Object) {\n *   get title() {\n *     return this._title;\n *   }\n *\n *   set title(title) {\n *     this._title = title;\n *     this.callObservers();\n *   }\n * }\n * ```\n *\n * ```\n * // TS\n *\n * import {Observable} from '@layr/observable';\n *\n * class Movie extends Observable(Object) {\n *   _title?: string;\n *\n *   get title() {\n *     return this._title;\n *   }\n *\n *   set title(title: string) {\n *     this._title = title;\n *     this.callObservers();\n *   }\n * }\n * ```\n *\n * Next, we can create a `Movie` instance, and observe it:\n *\n * ```\n * const movie = new Movie();\n *\n * movie.addObserver(() => {\n *   console.log('The movie's title has changed');\n * })\n * ```\n *\n * And now, every time we change the title of `movie`, its observer will be automatically executed:\n *\n * ```\n * movie.title = 'Inception';\n *\n * // Should display:\n * // 'The movie's title has changed'\n * ```\n *\n * > Note that the same result could have been achieved by using a Layr [`Component`](https://layrjs.com/docs/v2/reference/component):\n * >\n * > ```\n * > // JS\n * >\n * > import {Component, attribute} from '@layr/component';\n * >\n * > class Movie extends Component {\n * >   @attribute('string?') title;\n * > }\n * > ```\n * >\n * > ```\n * > // TS\n * >\n * > import {Component, attribute} from '@layr/component';\n * >\n * > class Movie extends Component {\n * >   @attribute('string?') title?: string;\n * > }\n * > ```\n *\n * ### Observable <badge type=\"primary\">class</badge> {#observable-class}\n *\n * An `Observable` class is constructed by calling the `Observable()` mixin ([see above](https://layrjs.com/docs/v2/reference/observable#observable-mixin)).\n * @mixin\n */\nexport function Observable<T extends Constructor>(Base: T) {\n  if (!isClass(Base)) {\n    throw new Error(\n      `The Observable mixin should be applied on a class (received type: '${getTypeOf(Base)}')`\n    );\n  }\n\n  if (typeof (Base as any).isObservable === 'function') {\n    return Base as T & typeof Observable;\n  }\n\n  const Observable = class extends Base {\n    /**\n     * Adds an observer to the current class or instance.\n     *\n     * @param observer A function that will be automatically executed when the [`callObservers()`](https://layrjs.com/docs/v2/reference/observable#call-observers-dual-method) method is called. Alternatively, you can specify an observable for which the observers should be executed, and doing so, you can connect an observable to another observable.\n     *\n     * @example\n     * ```\n     * Movie.addObserver(() => {\n     *   // A `Movie` class observer\n     * });\n     *\n     * const movie = new Movie();\n     *\n     * movie.addObserver(() => {\n     *   // A `Movie` instance observer\n     * });\n     *\n     * const actor = new Actor();\n     *\n     * // Connect `actor` to `movie` so that when `callObservers()` is called on `actor`,\n     * // then `callObservers()` is automatically called on `movie`\n     * actor.addObserver(movie);\n     * ```\n     *\n     * @category Methods\n     */\n    static get addObserver() {\n      return this.prototype.addObserver;\n    }\n\n    /**\n     * Adds an observer to the current class or instance.\n     *\n     * @param observer A function that will be automatically executed when the [`callObservers()`](https://layrjs.com/docs/v2/reference/observable#call-observers-dual-method) method is called. Alternatively, you can specify an observable for which the observers should be executed, and doing so, you can connect an observable to another observable.\n     *\n     * @example\n     * ```\n     * Movie.addObserver(() => {\n     *   // A `Movie` class observer\n     * });\n     *\n     * const movie = new Movie();\n     *\n     * movie.addObserver(() => {\n     *   // A `Movie` instance observer\n     * });\n     *\n     * const actor = new Actor();\n     *\n     * // Connect `actor` to `movie` so that when `callObservers()` is called on `actor`,\n     * // then `callObservers()` is automatically called on `movie`\n     * actor.addObserver(movie);\n     * ```\n     *\n     * @category Methods\n     */\n    addObserver(observer: Observer) {\n      this.__getObservers().add(observer);\n    }\n\n    /**\n     * Removes an observer from the current class or instance.\n     *\n     * @param observer A function or a connected observable.\n     *\n     * @example\n     * ```\n     * const observer = () => {\n     *   // ...\n     * }\n     *\n     * // Add `observer` to the `Movie` class\n     * Movie.addObserver(observer);\n     *\n     * // Remove `observer` from to the `Movie` class\n     * Movie.removeObserver(observer);\n     *\n     * const movie = new Movie();\n     * const actor = new Actor();\n     *\n     * // Connect `actor` to `movie`\n     * actor.addObserver(movie);\n     *\n     * // Remove the connection between `actor` and `movie`\n     * actor.removeObserver(movie);\n     * ```\n     *\n     * @category Methods\n     */\n    static get removeObserver() {\n      return this.prototype.removeObserver;\n    }\n\n    /**\n     * Removes an observer from the current class or instance.\n     *\n     * @param observer A function or a connected observable.\n     *\n     * @example\n     * ```\n     * const observer = () => {\n     *   // ...\n     * }\n     *\n     * // Add `observer` to the `Movie` class\n     * Movie.addObserver(observer);\n     *\n     * // Remove `observer` from to the `Movie` class\n     * Movie.removeObserver(observer);\n     *\n     * const movie = new Movie();\n     * const actor = new Actor();\n     *\n     * // Connect `actor` to `movie`\n     * actor.addObserver(movie);\n     *\n     * // Remove the connection between `actor` and `movie`\n     * actor.removeObserver(movie);\n     * ```\n     *\n     * @category Methods\n     */\n    removeObserver(observer: Observer) {\n      this.__getObservers().remove(observer);\n    }\n\n    /**\n     * Calls the observers of the current class or instance.\n     *\n     * @param [payload] An optional object to pass to the observers when they are executed.\n     *\n     * @example\n     * ```\n     * const movie = new Movie();\n     *\n     * movie.addObserver((payload) => {\n     *   console.log('Observer called with:', payload);\n     * });\n     *\n     * movie.callObservers();\n     *\n     * // Should display:\n     * // 'Observer called with: undefined'\n     *\n     * movie.callObservers({changes: ['title']});\n     *\n     * // Should display:\n     * // 'Observer called with: {changes: ['title']}'\n     * ```\n     *\n     * @category Methods\n     */\n    static get callObservers() {\n      return this.prototype.callObservers;\n    }\n\n    /**\n     * Calls the observers of the current class or instance.\n     *\n     * @param [payload] An optional object to pass to the observers when they are executed.\n     *\n     * @example\n     * ```\n     * const movie = new Movie();\n     *\n     * movie.addObserver((payload) => {\n     *   console.log('Observer called with:', payload);\n     * });\n     *\n     * movie.callObservers();\n     *\n     * // Should display:\n     * // 'Observer called with: undefined'\n     *\n     * movie.callObservers({changes: ['title']});\n     *\n     * // Should display:\n     * // 'Observer called with: {changes: ['title']}'\n     * ```\n     *\n     * @category Methods\n     */\n    callObservers(payload?: ObserverPayload) {\n      this.__getObservers().call(payload);\n    }\n\n    static __observers?: ObserverSet;\n\n    __observers?: ObserverSet;\n\n    static get __getObservers() {\n      return this.prototype.__getObservers;\n    }\n\n    __getObservers() {\n      if (!hasOwnProperty(this, '__observers')) {\n        Object.defineProperty(this, '__observers', {value: new ObserverSet()});\n      }\n\n      return this.__observers!;\n    }\n\n    static get isObservable() {\n      return this.prototype.isObservable;\n    }\n\n    isObservable(value: any): value is ObservableType {\n      return isObservable(value);\n    }\n  };\n\n  return Observable;\n}\n\n/**\n * Returns an observable from an existing object or array.\n *\n * The returned observable is observed deeply. So, for example, if an object contains a nested object, modifying the nested object will trigger the execution of the parent's observers.\n *\n * The returned observable provides the same methods as an [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) instance:\n *\n * - [`addObserver()`](https://layrjs.com/docs/v2/reference/observable#add-observer-dual-method)\n * - [`removeObserver()`](https://layrjs.com/docs/v2/reference/observable#remove-observer-dual-method)\n * - [`callObservers()`](https://layrjs.com/docs/v2/reference/observable#call-observers-dual-method)\n *\n * @param target A JavaScript plain object or array that you want to observe.\n *\n * @returns An observable objet or array.\n *\n * @example\n * ```\n * import {createObservable} from '@layr/observable';\n *\n * // Create an observable `movie`\n * const movie = createObservable({\n *   title: 'Inception',\n *   genres: ['drama'],\n *   details: {duration: 120}\n * });\n *\n * // Add an observer\n * movie.addObserver(() => {\n *   // ...\n * });\n *\n * // Then, any of the following changes on `movie` will call the observer:\n * movie.title = 'Inception 2';\n * delete movie.title;\n * movie.year = 2010;\n * movie.genres.push('action');\n * movie.genres[1] = 'sci-fi';\n * movie.details.duration = 125;\n * ```\n *\n * @category Bringing Observability to an Object or an Array\n */\nexport function createObservable<T extends object>(target: T) {\n  if (!canBeObserved(target)) {\n    throw new Error(\n      `Cannot create an observable from a target that is not an object, an array, or a function`\n    );\n  }\n\n  if (isObservable(target)) {\n    return target;\n  }\n\n  const observers = new ObserverSet();\n\n  const handleAddObserver = function (observer: Observer) {\n    observers.add(observer);\n  };\n\n  const handleRemoveObserver = function (observer: Observer) {\n    observers.remove(observer);\n  };\n\n  const handleCallObservers = function (payload?: ObserverPayload) {\n    observers.call(payload);\n  };\n\n  const handleIsObservable = function (value: any) {\n    return isObservable(value);\n  };\n\n  let observable: T & ObservableType;\n\n  const handler = {\n    has(target: object, key: string | number | symbol) {\n      if (\n        key === 'addObserver' ||\n        key === 'removeObserver' ||\n        key === 'callObservers' ||\n        key === 'isObservable'\n      ) {\n        return true;\n      }\n\n      return Reflect.has(target, key);\n    },\n\n    get(target: object, key: string | number | symbol, receiver?: any) {\n      if (receiver === observable) {\n        if (key === 'addObserver') {\n          return handleAddObserver;\n        }\n\n        if (key === 'removeObserver') {\n          return handleRemoveObserver;\n        }\n\n        if (key === 'callObservers') {\n          return handleCallObservers;\n        }\n\n        if (key === 'isObservable') {\n          return handleIsObservable;\n        }\n      }\n\n      return Reflect.get(target, key, receiver);\n    },\n\n    set(target: object, key: string | number | symbol, newValue: any, receiver?: any) {\n      if (\n        key === 'addObserver' ||\n        key === 'removeObserver' ||\n        key === 'callObservers' ||\n        key === 'isObservable'\n      ) {\n        throw new Error(\n          `Cannot set a property named 'addObserver', 'removeObserver', 'callObservers' or 'isObservable' in an observed object`\n        );\n      }\n\n      if (canBeObserved(newValue) && !isObservable(newValue) && isEmbeddable(newValue)) {\n        newValue = createObservable(newValue);\n      }\n\n      const previousValue = Reflect.get(target, key, receiver);\n\n      const result = Reflect.set(target, key, newValue, receiver);\n\n      if (receiver === observable && newValue?.valueOf() !== previousValue?.valueOf()) {\n        if (isObservable(previousValue) && isEmbeddable(previousValue)) {\n          previousValue.removeObserver(handleCallObservers);\n        }\n\n        if (isObservable(newValue) && isEmbeddable(newValue)) {\n          newValue.addObserver(handleCallObservers);\n        }\n\n        handleCallObservers();\n      }\n\n      return result;\n    },\n\n    deleteProperty(target: object, key: string | number | symbol) {\n      if (\n        key === 'addObserver' ||\n        key === 'removeObserver' ||\n        key === 'callObservers' ||\n        key === 'isObservable'\n      ) {\n        throw new Error(\n          `Cannot delete a property named 'addObserver', 'removeObserver', 'callObservers' or 'isObservable' in an observed object`\n        );\n      }\n\n      const previousValue = Reflect.get(target, key);\n\n      if (isObservable(previousValue) && isEmbeddable(previousValue)) {\n        previousValue.removeObserver(handleCallObservers);\n      }\n\n      const result = Reflect.deleteProperty(target, key);\n\n      handleCallObservers();\n\n      return result;\n    }\n  };\n\n  observable = new Proxy<T>(target, handler) as T & ObservableType;\n\n  const observeExistingValue = function (key: string | number, value: unknown) {\n    if (canBeObserved(value) && !isObservable(value) && isEmbeddable(value)) {\n      value = createObservable(value);\n      (target as any)[key] = value;\n    }\n\n    if (isObservable(value)) {\n      value.addObserver(observable);\n    }\n  };\n\n  if (Array.isArray(target)) {\n    for (let index = 0; index < target.length; index++) {\n      observeExistingValue(index, target[index]);\n    }\n  } else if (isPlainObject(target)) {\n    for (const [key, value] of Object.entries(target)) {\n      observeExistingValue(key, value);\n    }\n  }\n\n  return observable;\n}\n\nexport class ObserverSet {\n  _observers: Observer[];\n\n  constructor() {\n    this._observers = [];\n  }\n\n  add(observer: Observer) {\n    if (!(typeof observer === 'function' || isObservable(observer))) {\n      throw new Error(`Cannot add an observer that is not a function or an observable`);\n    }\n\n    this._observers.push(observer);\n  }\n\n  remove(observer: Observer) {\n    if (!(typeof observer === 'function' || isObservable(observer))) {\n      throw new Error(`Cannot remove an observer that is not a function or an observable`);\n    }\n\n    const index = this._observers.indexOf(observer);\n\n    if (index !== -1) {\n      this._observers.splice(index, 1);\n    }\n  }\n\n  call({\n    _observerStack = new Set(),\n    ...payload\n  }: ObserverPayload & {_observerStack?: Set<Observer>} = {}) {\n    for (const observer of this._observers) {\n      if (_observerStack.has(observer)) {\n        continue; // Avoid looping indefinitely when a circular reference is encountered\n      }\n\n      _observerStack.add(observer);\n\n      try {\n        if (isObservable(observer)) {\n          observer.callObservers({_observerStack, ...payload});\n        } else {\n          observer({_observerStack, ...payload});\n        }\n      } finally {\n        _observerStack.delete(observer);\n      }\n    }\n  }\n}\n\n/**\n * Returns whether the specified value is observable. When a value is observable, you can use any the following methods on it: [`addObserver()`](https://layrjs.com/docs/v2/reference/observable#add-observer-dual-method), [`removeObserver()`](https://layrjs.com/docs/v2/reference/observable#remove-observer-dual-method), and [`callObservers()`](https://layrjs.com/docs/v2/reference/observable#call-observers-dual-method).\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isObservable(value: any): value is ObservableType {\n  return typeof value?.isObservable === 'function';\n}\n\nexport function canBeObserved(value: any): value is object {\n  return (\n    (typeof value === 'object' && value !== null && !(value instanceof Date)) ||\n    typeof value === 'function'\n  );\n}\n\nexport function isEmbeddable(value: any) {\n  const isEmbedded = value?.constructor?.isEmbedded;\n\n  if (typeof isEmbedded === 'function' && isEmbedded() === false) {\n    return false;\n  }\n\n  return true;\n}\n"
  },
  {
    "path": "packages/observable/tsconfig.json",
    "content": "{\n  \"extends\": \"@mvila/tsconfig\",\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/react-integration/README.md",
    "content": "# @layr/react-integration\n\nReact integration for Layr.\n\n## Installation\n\n```\nnpm install @layr/react-integration\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/react-integration/package.json",
    "content": "{\n  \"name\": \"@layr/react-integration\",\n  \"version\": \"2.0.146\",\n  \"description\": \"React integration for Layr\",\n  \"keywords\": [\n    \"layr\",\n    \"react\",\n    \"integration\",\n    \"hook\",\n    \"navigator-plugin\"\n  ],\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/layrjs/layr/tree/master/packages/react-integration\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"dist/node-cjs/index.js\",\n  \"module\": \"dist/node-esm/index.js\",\n  \"engines\": {\n    \"node\": \">=16.0.0\"\n  },\n  \"scripts\": {\n    \"build\": \"dev-tools build:ts-library\",\n    \"prepare\": \"npm run build\",\n    \"publish:package\": \"dev-tools publish:package\",\n    \"update\": \"dev-tools update:dependencies\"\n  },\n  \"dependencies\": {\n    \"@layr/browser-navigator\": \"^2.0.67\",\n    \"@layr/component\": \"^2.0.51\",\n    \"@layr/memory-navigator\": \"^2.0.59\",\n    \"@layr/navigator\": \"^2.0.55\",\n    \"@layr/observable\": \"^1.0.16\",\n    \"@layr/routable\": \"^2.0.113\",\n    \"@layr/utilities\": \"^1.0.9\",\n    \"core-helpers\": \"^1.0.8\",\n    \"lodash\": \"^4.17.21\",\n    \"tslib\": \"^2.4.1\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.13.1\"\n  },\n  \"devDependencies\": {\n    \"@mvila/dev-tools\": \"^1.3.1\",\n    \"@mvila/tsconfig\": \"^1.0.6\",\n    \"@types/lodash\": \"^4.14.191\",\n    \"@types/react\": \"^16.14.34\"\n  }\n}\n"
  },
  {
    "path": "packages/react-integration/src/components.tsx",
    "content": "import {callRouteByURL, RoutableComponent, assertIsRoutableClass} from '@layr/routable';\nimport {Navigator} from '@layr/navigator';\nimport {BrowserNavigator} from '@layr/browser-navigator';\nimport {formatError} from '@layr/utilities';\nimport React, {useRef, useState, useEffect, useContext} from 'react';\n\nimport {useForceUpdate} from './hooks';\nimport {BrowserNavigatorPlugin} from './plugins';\n\n/**\n * A React component providing sensible defaults for a web app.\n *\n * You should use this component once at the top of your app.\n *\n * Note that if you use [Boostr](https://boostr.dev/) to manage your app development, this component will be automatically mounted, so you don't have to use it explicitly in your code.\n *\n * The main point of this component is to provide the default behavior of high-level hooks such as [`useData()`](https://layrjs.com/docs/v2/reference/react-integration#use-data-react-hook) or [`useAction()`](https://layrjs.com/docs/v2/reference/react-integration#use-action-react-hook):\n *\n * - `useData()` will render `null` while the `getter()` function is running, and, in the case an error is thrown, a `<div>` containing an error message will be rendered.\n * - `useAction()` will prevent the user from interacting with any UI element in the browser page while the `handler()` function is running, and, in the case an error is thrown, the browser's `alert()` function will be called to display the error message.\n *\n * @examplelink See an example of use in the [`BrowserNavigatorView`](https://layrjs.com/docs/v2/reference/react-integration#browser-navigator-view-react-component) React component.\n *\n * @category React Components\n * @reactcomponent\n */\nexport function BrowserRootView({\n  children,\n  ...customization\n}: {children: React.ReactNode} & Partial<Customization>) {\n  const previousCustomization = useContext(CustomizationContext);\n\n  if (previousCustomization !== undefined) {\n    throw new Error(\"An app shouldn't have more than one RootView\");\n  }\n\n  const actionView = useRef<BrowserActionView>(null);\n\n  return (\n    <CustomizationContext.Provider\n      value={{\n        dataPlaceholder: () => null,\n        errorRenderer: (error) => {\n          console.error(error);\n          return <div>{formatError(error)}</div>;\n        },\n        actionWrapper: async (actionHandler, args) => {\n          actionView.current!.open();\n          try {\n            return await actionHandler(...args);\n          } finally {\n            actionView.current!.close();\n          }\n        },\n        errorNotifier: async (error) => {\n          alert(formatError(error));\n        },\n        ...customization\n      }}\n    >\n      <BrowserActionView ref={actionView} />\n      {children}\n    </CustomizationContext.Provider>\n  );\n}\n\nexport const NavigatorContext = React.createContext<Navigator | undefined>(undefined);\n\n/**\n * A React component providing a [`BrowserNavigator`](https://layrjs.com/docs/v2/reference/browser-navigator#browser-navigator-class) to your app.\n *\n * You should use this component once at the top of your app after the [`BrowserRootView`](https://layrjs.com/docs/v2/reference/react-integration#browser-root-view-react-component) component.\n *\n * Note that if you use [Boostr](https://boostr.dev/) to manage your app development, this component will be automatically mounted, so you don't have to use it explicitly in your code.\n *\n * @param props.rootComponent The root Layr component of your app. Note that this Layr component should be [`Routable`](https://layrjs.com/docs/v2/reference/routable#routable-component-class).\n *\n * @example\n * ```\n * // JS\n *\n * import React, {Fragment} from 'react';\n * import ReactDOM from 'react-dom';\n * import {Component} from '@layr/component';\n * import {Routable} from '@layr/routable';\n * import {BrowserRootView, BrowserNavigatorView, layout, page} from '@layr/react-integration';\n *\n * class Application extends Routable(Component) {\n *   // `@layout('/')` is a shortcut for `@wrapper('/') @view()`\n *   ﹫layout('/') static MainLayout({children}) {\n *     return (\n *       <>\n *         <this.HomePage.Link>\n *           <h1>My App</h1>\n *         </this.HomePage.Link>\n *\n *         {children()} // Renders the subcomponents using this layout\n *       </>\n *     );\n *   }\n *\n *   // `@page('[/]')` is a shortcut for `@route('[/]') @view()`\n *   ﹫page('[/]') static HomePage() {\n *     return <p>Hello, World!</p>;\n *   }\n * }\n *\n * // Note that you don't need the following code when you use Boostr\n * ReactDOM.render(\n *   <BrowserRootView>\n *     <BrowserNavigatorView rootComponent={Application} />\n *   </BrowserRootView>,\n *   // Your `index.html` page should contain `<div id=\"root\"></div>`\n *   document.getElementById('root')\n * );\n * ```\n *\n * @example\n * ```\n * // TS\n *\n * import React, {Fragment} from 'react';\n * import ReactDOM from 'react-dom';\n * import {Component} from '@layr/component';\n * import {Routable} from '@layr/routable';\n * import {BrowserRootView, BrowserNavigatorView, layout, page} from '@layr/react-integration';\n *\n * class Application extends Routable(Component) {\n *   // `@layout('/')` is a shortcut for `@wrapper('/') @view()`\n *   ﹫layout('/') static MainLayout({children}: {children: () => any}) {\n *     return (\n *       <>\n *         <this.HomePage.Link>\n *           <h1>My App</h1>\n *         </this.HomePage.Link>\n *\n *         {children()} // Renders the subcomponents using this layout\n *       </>\n *     );\n *   }\n *\n *   // `@page('[/]')` is a shortcut for `@route('[/]') @view()`\n *   ﹫page('[/]') static HomePage() {\n *     return <p>Hello, World!</p>;\n *   }\n * }\n *\n * // Note that you don't need the following code when you use Boostr\n * ReactDOM.render(\n *   <BrowserRootView>\n *     <BrowserNavigatorView rootComponent={Application} />\n *   </BrowserRootView>,\n *   // Your `index.html` page should contain `<div id=\"root\"></div>`\n *   document.getElementById('root')\n * );\n * ```\n *\n * @category React Components\n * @reactcomponent\n */\nexport function BrowserNavigatorView({rootComponent}: {rootComponent: RoutableComponent}) {\n  assertIsRoutableClass(rootComponent);\n\n  const navigatorRef = useRef<BrowserNavigator>();\n\n  if (navigatorRef.current === undefined) {\n    navigatorRef.current = new BrowserNavigator({plugins: [BrowserNavigatorPlugin()]});\n    rootComponent.registerNavigator(navigatorRef.current);\n  }\n\n  const [isReady, setIsReady] = useState(false);\n\n  const forceUpdate = useForceUpdate();\n\n  useEffect(() => {\n    navigatorRef.current!.addObserver(forceUpdate);\n\n    setIsReady(true);\n\n    return function () {\n      navigatorRef.current!.removeObserver(forceUpdate);\n      navigatorRef.current!.unmount();\n    };\n  }, []);\n\n  if (!isReady) {\n    return null;\n  }\n\n  return (\n    <NavigatorContext.Provider value={navigatorRef.current}>\n      {callRouteByURL(rootComponent, navigatorRef.current.getCurrentURL())}\n    </NavigatorContext.Provider>\n  );\n}\n\n/**\n * A hook allowing you to get the [`Navigator`](https://layrjs.com/docs/v2/reference/navigator#navigator-class) used in your app.\n *\n * @returns A [`Navigator`](https://layrjs.com/docs/v2/reference/navigator#navigator-class) instance.\n *\n * @example\n * ```\n * import {Component} from '﹫layr/component';\n * import {Routable} from '﹫layr/routable';\n * import React from 'react';\n * import {view, useNavigator} from '﹫layr/react-integration';\n *\n * import logo from '../assets/app-logo.svg';\n *\n * class Application extends Routable(Component) {\n *   // ...\n *\n *   ﹫view() static LogoView() {\n *     const navigator = useNavigator();\n *\n *     return <img src={logo} onClick={() => { navigator.navigate('/); }} />;\n *   }\n * }\n * ```\n *\n * @category High-Level Hooks\n * @reacthook\n */\nexport function useNavigator() {\n  const navigator = useContext(NavigatorContext);\n\n  if (navigator === undefined) {\n    throw new Error(\n      \"Couldn't get a navigator. Please make sure you have included a NavigatorView at the top of your React component tree.\"\n    );\n  }\n\n  return navigator;\n}\n\nexport type Customization = {\n  dataPlaceholder: () => JSX.Element | null;\n  errorRenderer: (error: Error) => JSX.Element | null;\n  actionWrapper: (actionHandler: (...args: any[]) => Promise<any>, args: any[]) => Promise<any>;\n  errorNotifier: (error: Error) => Promise<void>;\n};\n\nexport const CustomizationContext = React.createContext<Customization | undefined>(undefined);\n\nexport function useCustomization() {\n  const customization = useContext(CustomizationContext);\n\n  if (customization === undefined) {\n    throw new Error(\n      \"Couldn't get the current customization. Please make sure you have included a RootView at the top of your React component tree.\"\n    );\n  }\n\n  return customization;\n}\n\n/**\n * A React component allowing you to customize the behavior of high-level hooks such as [`useData()`](https://layrjs.com/docs/v2/reference/react-integration#use-data-react-hook) or [`useAction()`](https://layrjs.com/docs/v2/reference/react-integration#use-action-react-hook).\n *\n * @param [props.dataPlaceholder] A function returning a React element (or `null`) that is rendered while the `getter()` function of the [`useData()`](https://layrjs.com/docs/v2/reference/react-integration#use-data-react-hook) hook is running. A typical use case is to render a spinner.\n * @param [props.errorRenderer] A function returning a React element (or `null`) that is rendered when the `getter()` function of the [`useData()`](https://layrjs.com/docs/v2/reference/react-integration#use-data-react-hook) hook throws an error. The `errorRenderer()` function receives the error as first parameter. A typical use case is to render an error message.\n * @param [props.actionWrapper] An asynchronous function allowing you to wrap the `handler()` function of the [`useAction()`](https://layrjs.com/docs/v2/reference/react-integration#use-data-react-hook) hook. The `actionWrapper()` function receives the `handler()` function as first parameter, should execute it, and return its result. A typical use case is to lock the screen while the `handler()` function is running so the user cannot interact with any UI element.\n * @param [props.errorNotifier] An asynchronous function that is executed when the `handler()` function of the [`useAction()`](https://layrjs.com/docs/v2/reference/react-integration#use-data-react-hook) hook throws an error. The `errorNotifier()` function receives the error as first parameter. A typical use case is to display an error alert dialog.\n *\n * @example\n * ```\n * <Customizer\n *   dataPlaceholder={() => {\n *     // Renders a custom `LoadingSpinner` component\n *     return <LoadingSpinner />;\n *   }}\n *   errorRenderer={(error) => {\n *     // Renders a custom `ErrorMessage` component\n *     return <ErrorMessage>{error}</ErrorMessage>;\n *   }}\n *   actionWrapper={async (actionHandler, args) => {\n *     // Do whatever you want here (e.g., custom screen locking)\n *     try {\n *       return await actionHandler(...args);\n *     } finally {\n *       // Do whatever you want here (e.g., custom screen unlocking)\n *     }\n *   }}\n *   errorNotifier={async (error) => {\n *     // Calls a custom `alert()` asynchronous function\n *     await alert(error.message);\n *   }}\n * >\n *   <YourChildComponent />\n * </Customizer>\n * ```\n *\n * @category React Components\n * @reactcomponent\n */\nexport function Customizer({\n  children,\n  ...customization\n}: Partial<Customization> & {children: React.ReactNode}) {\n  const previousCustomization = useCustomization();\n\n  return (\n    <CustomizationContext.Provider value={{...previousCustomization, ...customization}}>\n      {children}\n    </CustomizationContext.Provider>\n  );\n}\n\nexport class BrowserActionView extends React.Component<\n  {children?: React.ReactNode},\n  {count: number; activeElement: Element | null}\n> {\n  state = {\n    count: 0,\n    activeElement: null\n  };\n\n  open() {\n    this.setState(({count, activeElement}) => {\n      count++;\n\n      if (count === 1) {\n        activeElement = document.activeElement;\n\n        setTimeout(() => {\n          if (typeof (activeElement as any)?.blur === 'function') {\n            (activeElement as any).blur();\n          }\n        }, 0);\n      }\n\n      return {count, activeElement};\n    });\n  }\n\n  close() {\n    this.setState(({count, activeElement}) => {\n      count--;\n\n      if (count === 0) {\n        const savedActiveElement = activeElement;\n\n        setTimeout(() => {\n          if (typeof (savedActiveElement as any)?.focus === 'function') {\n            (savedActiveElement as any).focus();\n          }\n        }, 0);\n\n        activeElement = null;\n      }\n\n      return {count, activeElement};\n    });\n  }\n\n  render() {\n    if (this.state.count === 0) {\n      return null;\n    }\n\n    if (this.props.children !== undefined) {\n      return this.props.children;\n    }\n\n    return (\n      <div\n        style={{\n          position: 'fixed',\n          top: 0,\n          left: 0,\n          width: '100vw',\n          height: '100vh',\n          zIndex: 30000\n        }}\n      />\n    );\n  }\n}\n"
  },
  {
    "path": "packages/react-integration/src/decorators.tsx",
    "content": "import {Component, isComponentClassOrInstance} from '@layr/component';\nimport {\n  RoutableComponent,\n  route,\n  RouteOptions,\n  wrapper,\n  WrapperOptions,\n  Pattern\n} from '@layr/routable';\nimport {PlainObject, hasOwnProperty} from 'core-helpers';\n\nimport {useObserve} from './hooks';\n\ntype ViewOption = {observe?: boolean};\n\n/**\n * Decorates a method of a Layr [component](https://layrjs.com/docs/v2/reference/component) so it be can used as a React component.\n *\n * Like any React component, the method can receive some properties as first parameter and return some React elements to render.\n *\n * The decorator binds the method to a specific component, so when the method is executed by React (via, for example, a reference included in a [JSX expression](https://reactjs.org/docs/introducing-jsx.html)), it has access to the bound component through `this`.\n *\n * Also, the decorator observes the attributes of the bound component, so when the value of an attribute changes, the React component is automatically re-rendered.\n *\n * @example\n * ```\n * import {Component, attribute} from '﹫layr/component';\n * import React from 'react';\n * import ReactDOM from 'react-dom';\n * import {view} from '﹫layr/react-integration';\n *\n * class Person extends Component {\n *   ﹫attribute('string') firstName = '';\n *\n *   ﹫attribute('string') lastName = '';\n *\n *   ﹫view() FullName() {\n *     return <span>{`${this.firstName} ${this.fullName}`}</span>;\n *   }\n * }\n *\n * const person = new Person({firstName: 'Alan', lastName: 'Turing'});\n *\n * ReactDOM.render(<person.FullName />, document.getElementById('root'));\n * ```\n *\n * @category Decorators\n * @decorator\n */\nexport function view(options: ViewOption = {}) {\n  const {observe = true} = options;\n\n  return function (\n    target: typeof Component | Component,\n    name: string,\n    descriptor: PropertyDescriptor\n  ) {\n    const {value: ReactComponent, configurable, enumerable} = descriptor;\n\n    if (\n      !(\n        isComponentClassOrInstance(target) &&\n        typeof ReactComponent === 'function' &&\n        enumerable === false\n      )\n    ) {\n      throw new Error(\n        `@view() should be used to decorate a component method (property: '${name}')`\n      );\n    }\n\n    return {\n      configurable,\n      enumerable,\n      get(this: (typeof Component | Component) & {__boundReactComponents: PlainObject}) {\n        if (!hasOwnProperty(this, '__boundReactComponents')) {\n          Object.defineProperty(this, '__boundReactComponents', {value: Object.create(null)});\n        }\n\n        let BoundReactComponent = this.__boundReactComponents[name];\n\n        if (BoundReactComponent === undefined) {\n          BoundReactComponent = (...args: any[]) => {\n            if (observe) {\n              useObserve(this);\n            }\n\n            return ReactComponent.apply(this, args);\n          };\n\n          BoundReactComponent.displayName = this.describeComponentProperty(name);\n\n          Object.defineProperty(BoundReactComponent, '__isView', {value: true});\n\n          this.__boundReactComponents[name] = BoundReactComponent;\n        }\n\n        return BoundReactComponent;\n      }\n    };\n  };\n}\n\n/**\n * A convenience decorator that combines the [`@route()`](https://layrjs.com/docs/v2/reference/routable#route-decorator) and [`@view()`](https://layrjs.com/docs/v2/reference/react-integration#view-decorator) decorators.\n *\n * Typically, you should use this decorator to implement the pages of your app.\n *\n * @param pattern The canonical [URL pattern](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type) of the route.\n * @param [options] An object specifying the options to pass to the `Route`'s [constructor](https://layrjs.com/docs/v2/reference/addressable#constructor) when the route is created.\n *\n * @examplelink See an example of use in the [`BrowserNavigatorView`](https://layrjs.com/docs/v2/reference/react-integration#browser-navigator-view-react-component) React component.\n *\n * @category Decorators\n * @decorator\n */\nexport function page(pattern: Pattern, options: ViewOption & RouteOptions = {}) {\n  return function (\n    target: typeof RoutableComponent | RoutableComponent,\n    name: string,\n    descriptor: PropertyDescriptor\n  ) {\n    descriptor = view(options)(target, name, descriptor);\n    descriptor = route(pattern, options)(target, name, descriptor);\n    return descriptor;\n  };\n}\n\n/**\n * A convenience decorator that combines the [`@wrapper()`](https://layrjs.com/docs/v2/reference/routable#wrapper-decorator) and [`@view()`](https://layrjs.com/docs/v2/reference/react-integration#view-decorator) decorators.\n *\n * Typically, you should use this decorator to implement the layouts of your app.\n *\n * @param pattern The canonical [URL pattern](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type) of the wrapper.\n * @param [options] An object specifying the options to pass to the `Wrapper`'s [constructor](https://layrjs.com/docs/v2/reference/addressable#constructor) when the wrapper is created.\n *\n * @examplelink See an example of use in the [`BrowserNavigatorView`](https://layrjs.com/docs/v2/reference/react-integration#browser-navigator-view-react-component) React component.\n *\n * @category Decorators\n * @decorator\n */\nexport function layout(pattern: Pattern, options: ViewOption & WrapperOptions = {}) {\n  return function (\n    target: typeof RoutableComponent | RoutableComponent,\n    name: string,\n    descriptor: PropertyDescriptor\n  ) {\n    descriptor = view(options)(target, name, descriptor);\n    descriptor = wrapper(pattern, options)(target, name, descriptor);\n    return descriptor;\n  };\n}\n"
  },
  {
    "path": "packages/react-integration/src/hooks.ts",
    "content": "import {ObservableType, isObservable} from '@layr/observable';\nimport {useState, useEffect, useCallback, useRef, useMemo, DependencyList} from 'react';\nimport {AsyncFunction, getTypeOf} from 'core-helpers';\n\nimport {useCustomization, Customization} from './components';\n\n/**\n * A convenience hook for loading data asynchronously and rendering a React element using the loaded data.\n *\n * The `getter()` asynchronous function is called when the React component is rendered for the first time and when a change is detected in its `dependencies`.\n *\n * While the `getter()` function is running, the `useData()` hook returns the result of the nearest `dataPlaceholder()` function, which can be defined in any parent component thanks to the [`Customizer`](https://layrjs.com/docs/v2/reference/react-integration#customizer-react-component) component.\n *\n * Once the `getter()` function is executed, the `useData()` hook returns the result of the `renderer()` function which is called with the result of the `getter()` function as first parameter.\n *\n * If an error occurs during the `getter()` function execution, the `useData()` hook returns the result of the nearest `errorRenderer()` function, which can be defined in any parent component thanks to the [`Customizer`](https://layrjs.com/docs/v2/reference/react-integration#customizer-react-component) component.\n *\n * @param getter An asynchronous function for loading data.\n * @param renderer A function which is called with the result of the `getter()` function as first parameter and a `refresh()` function as second parameter. You can call the `refresh()` function to force the re-execution of the `getter()` function. The `renderer()` function should return a React element (or `null`).\n * @param [dependencies] An array of values on which the `getter()` function depends (default: `[]`).\n * @param [options.dataPlaceholder] A custom `dataPlaceholder()` function.\n * @param [options.errorRenderer] A custom `errorRenderer()` function.\n *\n * @returns A React element (or `null`).\n *\n * @example\n * ```\n * import {Component} from '﹫layr/component';\n * import React from 'react';\n * import {view, useData} from '﹫layr/react-integration';\n *\n * class Article extends Component {\n *   // ...\n *\n *   ﹫view() static List() {\n *     return useData(\n *       async () => {\n *         // Return some articles from the backend\n *       },\n *\n *       (articles) => {\n *         return articles.map((article) => (\n *           <div key={article.id}>{article.title}</div>\n *         ));\n *       }\n *     );\n *   }\n * }\n * ```\n *\n * @category High-Level Hooks\n * @reacthook\n */\nexport function useData<Result>(\n  getter: () => Promise<Result>,\n  renderer: (data: Result, refresh: () => void) => JSX.Element | null,\n  deps: DependencyList = [],\n  options: {\n    dataPlaceholder?: Customization['dataPlaceholder'];\n    errorRenderer?: Customization['errorRenderer'];\n  } = {}\n) {\n  const {dataPlaceholder, errorRenderer} = {...useCustomization(), ...options};\n\n  const [data, isExecuting, error, refresh] = useAsyncMemo(getter, deps);\n\n  if (isExecuting) {\n    return dataPlaceholder();\n  }\n\n  if (error !== undefined) {\n    return errorRenderer(error);\n  }\n\n  return renderer(data!, refresh);\n}\n\n/**\n * A convenience hook for executing some asynchronous actions.\n *\n * The specified `handler()` asynchronous function is wrapped so that:\n *\n * - When running, the screen is locked to prevent the user from interacting with any UI element. You can customize the screen locking mechanism in any parent component thanks to the [Customizer's `actionWrapper()`](https://layrjs.com/docs/v2/reference/react-integration#customizer-react-component) prop.\n * - In case an error is thrown, an error alert dialog is displayed. You can customize the error alert dialog in any parent component thanks to the [Customizer's `errorNotifier()`](https://layrjs.com/docs/v2/reference/react-integration#customizer-react-component) prop.\n *\n * @param handler An asynchronous function implementing the action.\n * @param [dependencies] An array of values on which the `handler()` function depends (default: `[]`).\n * @param [options.actionWrapper] A custom `actionWrapper()` function.\n * @param [options.errorNotifier] A custom `errorNotifier()` function.\n *\n * @returns An asynchronous function wrapping the specified `handler()`.\n *\n * @example\n * ```\n * import {Component} from '﹫layr/component';\n * import React from 'react';\n * import {view, useAction} from '﹫layr/react-integration';\n *\n * class Article extends Component {\n *   // ...\n *\n *   ﹫view() EditView() {\n *     const save = useAction(async () => {\n *       // Save the edited article to the backend\n *     });\n *\n *     return (\n *       <form\n *         onSubmit={(event) => {\n *           event.preventDefault();\n *           save();\n *         }}\n *       >\n *         <div>\n *           Implement your form fields here.\n *         </div>\n *\n *         <div>\n *           <button type=\"submit\">Save</button>\n *         </div>\n *       </form>\n *     );\n *   }\n * }\n * ```\n *\n * @category High-Level Hooks\n * @reacthook\n */\nexport function useAction<Args extends any[] = any[], Result = any>(\n  handler: AsyncFunction<Args, Result>,\n  deps: DependencyList = [],\n  options: {\n    actionWrapper?: Customization['actionWrapper'];\n    errorNotifier?: Customization['errorNotifier'];\n  } = {}\n) {\n  const {actionWrapper, errorNotifier} = {...useCustomization(), ...options};\n\n  const action = useCallback(async (...args: Args) => {\n    try {\n      return (await actionWrapper(handler as (...args: any[]) => Promise<any>, args)) as Result;\n    } catch (error: any) {\n      await errorNotifier(error);\n      throw error;\n    }\n  }, deps);\n\n  return action;\n}\n\n/**\n * Makes a view dependent of an [observable](https://layrjs.com/docs/v2/reference/observable#observable-type) so the view is automatically re-rendered when the observable changes.\n *\n * @param observable An [observable](https://layrjs.com/docs/v2/reference/observable#observable-type) object.\n *\n * @example\n * ```\n * import {Component} from '﹫layr/component';\n * import {createObservable} from '﹫layr/observable';\n * import React from 'react';\n * import {view, useObserve} from '﹫layr/react-integration';\n *\n * const observableArray = createObservable([]);\n *\n * class MyComponent extends Component {\n *   ﹫view() static View() {\n *     useObserve(observableArray);\n *\n *     return (\n *       <div>\n *         {`observableArray's length: ${observableArray.length}`}\n *       </div>\n *     );\n *   }\n * }\n *\n * // Changing `observableArray` will re-render `MyComponent.View`\n * observableArray.push('abc');\n * ```\n *\n * @category High-Level Hooks\n * @reacthook\n */\nexport function useObserve(observable: ObservableType) {\n  if (!isObservable(observable)) {\n    throw new Error(\n      `Expected an observable class or instance, but received a value of type '${getTypeOf(\n        observable\n      )}'`\n    );\n  }\n\n  const forceUpdate = useForceUpdate();\n\n  useEffect(\n    function () {\n      observable.addObserver(forceUpdate);\n\n      return function () {\n        observable.removeObserver(forceUpdate);\n      };\n    },\n    [observable]\n  );\n}\n\n/**\n * Allows you to define an asynchronous callback and keep track of its execution.\n *\n * Plays the same role as the React built-in [`useCallback()`](https://reactjs.org/docs/hooks-reference.html#usecallback) hook but works with asynchronous callbacks.\n *\n * @param asyncCallback An asynchronous callback.\n * @param [dependencies] An array of values on which the asynchronous callback depends (default: `[]`).\n *\n * @returns An array of the shape `[trackedCallback, isExecuting, error, result]` where `trackedCallback` is a function that you can call to execute the asynchronous callback, `isExecuting` is a boolean indicating whether the asynchronous callback is being executed, `error` is the error thrown by the asynchronous callback in case of failed execution, and `result` is the value returned by the asynchronous callback in case of succeeded execution.\n *\n * @example\n * ```\n * import {Component} from '﹫layr/component';\n * import React from 'react';\n * import {view, useAsyncCallback} from '﹫layr/react-integration';\n *\n * class Article extends Component {\n *   ﹫view() UpvoteButton() {\n *     const [handleUpvote, isUpvoting, upvotingError] = useAsyncCallback(async () => {\n *       await this.upvote();\n *     });\n *\n *     return (\n *       <div>\n *         <button onClick={handleUpvote} disabled={isUpvoting}>Upvote</button>\n *         {upvotingError && ' An error occurred while upvoting the article.'}\n *       </div>\n *     );\n *   }\n * }\n * ```\n *\n * @category Low-Level Hooks\n * @reacthook\n */\nexport function useAsyncCallback<Args extends any[] = any[], Result = any>(\n  asyncCallback: AsyncFunction<Args, Result>,\n  deps: DependencyList = []\n) {\n  const [state, setState] = useState<{isExecuting?: boolean; error?: any; result?: Result}>({});\n\n  const isMounted = useIsMounted();\n\n  const trackedCallback = useCallback(\n    async (...args: Args) => {\n      setState({isExecuting: true});\n\n      try {\n        const result = await asyncCallback(...args);\n\n        if (isMounted()) {\n          setState({result});\n        }\n\n        return result;\n      } catch (error) {\n        if (isMounted()) {\n          setState({error});\n        }\n\n        throw error;\n      }\n    },\n    [...deps]\n  );\n\n  return [trackedCallback, state.isExecuting === true, state.error, state.result] as const;\n}\n\n/**\n * Memoizes the result of an asynchronous function execution and provides a \"recompute function\" that you can call to recompute the memoized result.\n *\n * The asynchronous function is executed one time when the React component is rendered for the first time, and each time a dependency is changed or the \"recompute function\" is called.\n *\n * Plays the same role as the React built-in [`useMemo()`](https://reactjs.org/docs/hooks-reference.html#usememo) hook but works with asynchronous functions and allows to recompute the memoized result.\n *\n * @param asyncFunc An asynchronous function to compute the memoized result.\n * @param [dependencies] An array of values on which the memoized result depends (default: `[]`, which means that the memoized result will be recomputed only when the \"recompute function\" is called).\n *\n * @returns An array of the shape `[memoizedResult, isExecuting, error, recompute]` where `memoizedResult` is the result returned by the asynchronous function in case of succeeded execution, `isExecuting` is a boolean indicating whether the asynchronous function is being executed, `error` is the error thrown by the asynchronous function in case of failed execution, and `recompute` is a function that you can call to recompute the memoized result.\n *\n * @example\n * ```\n * import {Component} from '﹫layr/component';\n * import React from 'react';\n * import {view, useAsyncMemo} from '﹫layr/react-integration';\n *\n * class Article extends Component {\n *   // ...\n *\n *   ﹫view() static List() {\n *     const [articles, isLoading, loadingError, retryLoading] = useAsyncMemo(\n *       async () => {\n *         // Return some articles from the backend\n *       }\n *     );\n *\n *     if (isLoading) {\n *       return <div>Loading the articles...</div>;\n *     }\n *\n *     if (loadingError) {\n *       return (\n *         <div>\n *           An error occurred while loading the articles.\n *           <button onClick={retryLoading}>Retry</button>\n *         </div>\n *       );\n *     }\n *\n *     return articles.map((article) => (\n *       <div key={article.id}>{article.title}</div>\n *     ));\n *   }\n * }\n * ```\n *\n * @category Low-Level Hooks\n * @reacthook\n */\nexport function useAsyncMemo<Result>(asyncFunc: () => Promise<Result>, deps: DependencyList = []) {\n  const [state, setState] = useState<{\n    result?: Result;\n    isExecuting?: boolean;\n    error?: any;\n  }>({isExecuting: true});\n\n  const [recomputeCount, setRecomputeCount] = useState(0);\n\n  const isMounted = useIsMounted();\n\n  useEffect(() => {\n    setState({isExecuting: true});\n\n    asyncFunc().then(\n      (result) => {\n        if (isMounted()) {\n          setState({result});\n        }\n\n        return result;\n      },\n      (error) => {\n        if (isMounted()) {\n          setState({error});\n        }\n\n        throw error;\n      }\n    );\n  }, [...deps, recomputeCount]);\n\n  const recompute = useCallback(() => {\n    setState({isExecuting: true});\n    setRecomputeCount((recomputeCount) => recomputeCount + 1);\n  }, []);\n\n  return [state.result, state.isExecuting === true, state.error, recompute] as const;\n}\n\n/**\n * Memoizes the result of a function execution and provides a \"recompute function\" that you can call to recompute the memoized result.\n *\n * The function is executed one time when the React component is rendered for the first time, and each time a dependency is changed or the \"recompute function\" is called.\n *\n * Plays the same role as the React built-in [`useMemo()`](https://reactjs.org/docs/hooks-reference.html#usememo) hook but with the extra ability to recompute the memoized result.\n *\n * @param func A function to compute the memoized result.\n * @param [dependencies] An array of values on which the memoized result depends (default: `[]`, which means that the memoized result will be recomputed only when the \"recompute function\" is called).\n *\n * @returns An array of the shape `[memoizedResult, recompute]` where `memoizedResult` is the result of the function execution, and `recompute` is a function that you can call to recompute the memoized result.\n *\n * @example\n * ```\n * import {Component, provide} from '﹫layr/component';\n * import React, {useCallback} from 'react';\n * import {view, useRecomputableMemo} from '﹫layr/react-integration';\n *\n * class Article extends Component {\n *   // ...\n * }\n *\n * class Blog extends Component {\n *   ﹫provide() static Article = Article;\n *\n *   ﹫view() static CreateArticleView() {\n *     const [article, resetArticle] = useRecomputableMemo(() => new Article());\n *\n *     const createArticle = useCallback(async () => {\n *       // Save the created article to the backend\n *       resetArticle();\n *     }, [article]);\n *\n *     return (\n *       <div>\n *         <article.CreateForm onSubmit={createArticle} />\n *       </div>\n *     );\n *   }\n * }\n * ```\n *\n * @category Low-Level Hooks\n * @reacthook\n */\nexport function useRecomputableMemo<Result>(func: () => Result, deps: DependencyList = []) {\n  const [recomputeCount, setRecomputeCount] = useState(0);\n\n  const result = useMemo(func, [...deps, recomputeCount]);\n\n  const recompute = useCallback(() => {\n    setRecomputeCount((recomputeCount) => recomputeCount + 1);\n  }, []);\n\n  return [result, recompute] as const;\n}\n\n/**\n * Allows you to call an asynchronous function and keep track of its execution.\n *\n * The function is executed one time when the React component is rendered for the first time, and each time a dependency is changed or the \"recall function\" is called.\n *\n * @param asyncFunc The asynchronous function to call.\n * @param [dependencies] An array of values on which the asynchronous function depends (default: `[]`, which means that the asynchronous will be recalled only when the \"recall function\" is called).\n *\n * @returns An array of the shape `[isExecuting, error, recall]` where `isExecuting` is a boolean indicating whether the asynchronous function is being executed, `error` is the error thrown by the asynchronous function in case of failed execution, and `recall` is a function that you can call to recall the asynchronous function.\n *\n * @example\n * ```\n * // JS\n *\n * import {Component, provide, attribute} from '﹫layr/component';\n * import React from 'react';\n * import {view, useAsyncCall} from '﹫layr/react-integration';\n *\n * class Article extends Component {\n *   // ...\n * }\n *\n * class Blog extends Component {\n *   ﹫provide() static Article = Article;\n *\n *   ﹫attribute('Article[]?') static loadedArticles;\n *\n *   ﹫view() static View() {\n *     const [isLoading, loadingError, retryLoading] = useAsyncCall(\n *       async () => {\n *         this.loadedArticles = await this.Article.find();\n *       }\n *     );\n *\n *     if (isLoading) {\n *       return <div>Loading the articles...</div>;\n *     }\n *\n *     if (loadingError) {\n *       return (\n *         <div>\n *           An error occurred while loading the articles.\n *           <button onClick={retryLoading}>Retry</button>\n *         </div>\n *       );\n *     }\n *\n *     return this.loadedArticles.map((article) => (\n *       <div key={article.id}>{article.title}</div>\n *     ));\n *   }\n * }\n * ```\n *\n * @example\n * ```\n * // TS\n *\n * import {Component, provide, attribute} from '﹫layr/component';\n * import React from 'react';\n * import {view, useAsyncCall} from '﹫layr/react-integration';\n *\n * class Article extends Component {\n *   // ...\n * }\n *\n * class Blog extends Component {\n *   ﹫provide() static Article = Article;\n *\n *   ﹫attribute('Article[]?') static loadedArticles?: Article[];\n *\n *   ﹫view() static View() {\n *     const [isLoading, loadingError, retryLoading] = useAsyncCall(\n *       async () => {\n *         this.loadedArticles = await this.Article.find();\n *       }\n *     );\n *\n *     if (isLoading) {\n *       return <div>Loading the articles...</div>;\n *     }\n *\n *     if (loadingError) {\n *       return (\n *         <div>\n *           An error occurred while loading the articles.\n *           <button onClick={retryLoading}>Retry</button>\n *         </div>\n *       );\n *     }\n *\n *     return this.loadedArticles!.map((article) => (\n *       <div key={article.id}>{article.title}</div>\n *     ));\n *   }\n * }\n * ```\n *\n * @category Low-Level Hooks\n * @reacthook\n */\nexport function useAsyncCall(asyncFunc: () => Promise<void>, deps: DependencyList = []) {\n  const [, isExecuting, error, recall] = useAsyncMemo(asyncFunc, deps);\n\n  return [isExecuting, error, recall] as const;\n}\n\nexport function useIsMounted() {\n  const isMountedRef = useRef(false);\n\n  const isMounted = useCallback(() => {\n    return isMountedRef.current;\n  }, []);\n\n  useEffect(() => {\n    isMountedRef.current = true;\n\n    return () => {\n      isMountedRef.current = false;\n    };\n  }, []);\n\n  return isMounted;\n}\n\nexport function useForceUpdate() {\n  const [, setState] = useState({});\n  const isMounted = useIsMounted();\n\n  const forceUpdate = useCallback(() => {\n    if (isMounted()) {\n      setState({});\n    }\n  }, []);\n\n  return forceUpdate;\n}\n\nexport function useDelay(duration = 100) {\n  const [isElapsed, setIsElapsed] = useState(false);\n\n  useEffect(() => {\n    const timeout = setTimeout(() => {\n      setIsElapsed(true);\n    }, duration);\n\n    return () => {\n      clearTimeout(timeout);\n    };\n  }, []);\n\n  return [isElapsed] as const;\n}\n"
  },
  {
    "path": "packages/react-integration/src/index.ts",
    "content": "/**\n * @module react-integration\n *\n * Provides some React components, hooks, and decorators to simplify the use of [React](https://reactjs.org/) inside a Layr app.\n */\n\nexport * from './components';\nexport * from './decorators';\nexport * from './hooks';\nexport * from './plugins';\n"
  },
  {
    "path": "packages/react-integration/src/plugins.tsx",
    "content": "import {Navigator, normalizeURL} from '@layr/navigator';\nimport {BrowserNavigatorLinkProps} from '@layr/browser-navigator';\nimport React, {useMemo, useCallback, FunctionComponent} from 'react';\nimport {hasOwnProperty} from 'core-helpers';\n\nexport function BrowserNavigatorPlugin() {\n  return function (navigator: Navigator) {\n    navigator.addAddressableMethodWrapper(function (receiver, method, params) {\n      if (hasOwnProperty(method, '__isView')) {\n        return React.createElement(method as FunctionComponent, params);\n      } else {\n        return method.call(receiver, params);\n      }\n    });\n\n    navigator.addCustomRouteDecorator(function (method) {\n      method.Link = function ({params, hash, ...props}) {\n        const to = method.generateURL(params, {hash});\n\n        return navigator.Link({to, ...props});\n      };\n    });\n\n    Object.assign(navigator, {\n      Link(props: BrowserNavigatorLinkProps) {\n        const {to, className, activeClassName, style, activeStyle, ...otherProps} = props;\n\n        if ('onClick' in props) {\n          throw new Error(`The 'onClick' prop is not allowed in the 'Link' component`);\n        }\n\n        const handleClick = useCallback(\n          (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {\n            if (!(event.shiftKey || event.ctrlKey || event.altKey || event.metaKey)) {\n              event.preventDefault();\n\n              navigator.navigate(to, {defer: false});\n            }\n          },\n          [to]\n        );\n\n        const currentPath = navigator.getCurrentPath();\n        const linkPath = normalizeURL(to).pathname;\n        const isActive = linkPath === currentPath;\n\n        const {actualClassName, actualStyle} = useMemo(() => {\n          let actualClassName = className;\n          let actualStyle = style;\n\n          if (isActive) {\n            if (activeClassName !== undefined) {\n              const classes = actualClassName !== undefined ? [actualClassName] : [];\n              classes.push(activeClassName);\n              actualClassName = classes.join(' ');\n            }\n\n            if (activeStyle !== undefined) {\n              actualStyle = {...actualStyle, ...activeStyle};\n            }\n          }\n\n          return {actualClassName, actualStyle};\n        }, [isActive, className, activeClassName, style, activeStyle]);\n\n        return (\n          <a\n            href={to}\n            onClick={handleClick}\n            className={actualClassName}\n            style={actualStyle}\n            {...otherProps}\n          />\n        );\n      }\n    });\n  };\n}\n"
  },
  {
    "path": "packages/react-integration/tsconfig.json",
    "content": "{\n  \"extends\": \"@mvila/tsconfig\",\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/routable/.gitignore",
    "content": ".DS_STORE\nnode_modules\n*.log\n/dist\n"
  },
  {
    "path": "packages/routable/README.md",
    "content": "# @layr/routable\n\nMakes the methods of your Layr components callable by URL.\n\n## Installation\n\n```\nnpm install @layr/routable\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/routable/package.json",
    "content": "{\n  \"name\": \"@layr/routable\",\n  \"version\": \"2.0.113\",\n  \"description\": \"Makes the methods of your Layr components callable by URL\",\n  \"keywords\": [\n    \"layr\",\n    \"component\",\n    \"router\",\n    \"routable\",\n    \"route\",\n    \"method\"\n  ],\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/layrjs/layr/tree/master/packages/routable\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"dist/node-cjs/index.js\",\n  \"module\": \"dist/node-esm/index.js\",\n  \"engines\": {\n    \"node\": \">=16.0.0\"\n  },\n  \"scripts\": {\n    \"build\": \"dev-tools build:ts-library\",\n    \"prepare\": \"npm run build\",\n    \"test\": \"dev-tools test:ts-library\",\n    \"publish:package\": \"dev-tools publish:package\",\n    \"update\": \"dev-tools update:dependencies\"\n  },\n  \"dependencies\": {\n    \"@layr/component\": \"^2.0.51\",\n    \"@layr/navigator\": \"^2.0.55\",\n    \"core-helpers\": \"^1.0.8\",\n    \"debug\": \"^4.3.4\",\n    \"lodash\": \"^4.17.21\",\n    \"possibly-async\": \"^1.0.7\",\n    \"tslib\": \"^2.4.1\"\n  },\n  \"devDependencies\": {\n    \"@mvila/dev-tools\": \"^1.3.1\",\n    \"@mvila/tsconfig\": \"^1.0.6\",\n    \"@types/debug\": \"^4.1.7\",\n    \"@types/http-errors\": \"^1.8.2\",\n    \"@types/jest\": \"^29.2.5\",\n    \"@types/lodash\": \"^4.14.191\",\n    \"http-errors\": \"^1.8.1\"\n  }\n}\n"
  },
  {
    "path": "packages/routable/src/addressable.ts",
    "content": "import {normalizeURL, parseQuery, stringifyQuery, URLOptions} from '@layr/navigator';\nimport {possiblyAsync} from 'possibly-async';\nimport isEmpty from 'lodash/isEmpty';\n\nimport {parsePattern, Pattern, PathMatcher, PathGenerator} from './pattern';\nimport {\n  serializeParam,\n  deserializeParam,\n  parseParamTypeSpecifier,\n  Params,\n  ParamTypeDescriptor\n} from './param';\n\nexport type AddressableOptions = {\n  params?: Params;\n  aliases?: Pattern[];\n  filter?: Filter;\n  transformers?: Transformers;\n};\n\nexport type Filter = (request: any) => boolean;\n\nexport type Transformers = {\n  input?: (params?: any, request?: any) => any;\n  output?: (result?: any, request?: any) => any;\n  error?: (error?: any, request?: any) => any;\n};\n\n/**\n * An abstract class from which the classes [`Route`](https://layrjs.com/docs/v2/reference/route) and [`Wrapper`](https://layrjs.com/docs/v2/reference/wrapper) are constructed.\n *\n * An addressable is composed of:\n *\n * - A name matching a method of a [routable component](https://layrjs.com/docs/v2/reference/routable#routable-component-class).\n * - The canonical [URL pattern](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type) of the addressable.\n * - Some optional [URL parameters](https://layrjs.com/docs/v2/reference/addressable#url-parameters-type) associated with the addressable.\n * - Some optional [URL pattern aliases](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type) associated with the addressable.\n *\n * #### Usage\n *\n * Typically, you create a `Route` or a `Wrapper` and associate it to a routable component by using the [`@route()`](https://layrjs.com/docs/v2/reference/routable#route-decorator) or [`@wrapper()`](https://layrjs.com/docs/v2/reference/routable#wrapper-decorator) decorators.\n *\n * See an example of use in the [`BrowserNavigatorView`](https://layrjs.com/docs/v2/reference/react-integration#browser-navigator-view-react-component) React component.\n */\nexport abstract class Addressable {\n  _name: string;\n  _patterns: {\n    pattern: Pattern;\n    matcher: PathMatcher;\n    generator: PathGenerator;\n    wrapperGenerator: PathGenerator;\n  }[];\n  _isCatchAll: boolean;\n  _params: Record<string, ParamTypeDescriptor>;\n  _filter: Filter | undefined;\n  _transformers: Transformers;\n\n  /**\n   * Creates an instance of [`Addressable`](https://layrjs.com/docs/v2/reference/addressable), which can represent a [`Route`](https://layrjs.com/docs/v2/reference/route) or a [`Wrapper`](https://layrjs.com/docs/v2/reference/wrapper).\n   *\n   * Typically, instead of using this constructor, you would rather use the [`@route()`](https://layrjs.com/docs/v2/reference/routable#route-decorator) or [`@wrapper()`](https://layrjs.com/docs/v2/reference/routable#wrapper-decorator) decorators.\n   *\n   * @param name The name of the addressable.\n   * @param pattern The canonical [URL pattern](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type) of the addressable.\n   * @param [options.parameters] An optional object containing some [URL parameters](https://layrjs.com/docs/v2/reference/addressable#url-parameters-type).\n   * @param [options.aliases] An optional array containing some [URL pattern aliases](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type).\n   *\n   * @returns The [`Addressable`](https://layrjs.com/docs/v2/reference/addressable) instance that was created.\n   *\n   * @example\n   * ```\n   * const addressable = new Addressable('View', '/', {aliases: ['/home']});\n   * ```\n   *\n   * @category Creation\n   */\n  constructor(name: string, pattern: Pattern, options: AddressableOptions = {}) {\n    const {params = {}, aliases = [], filter, transformers = {}} = options;\n\n    this._name = name;\n\n    this._params = Object.create(null);\n\n    for (const [name, typeSpecifier] of Object.entries(params)) {\n      this._params[name] = {\n        ...parseParamTypeSpecifier(typeSpecifier),\n        specifier: typeSpecifier\n      };\n    }\n\n    this._patterns = [];\n\n    this._isCatchAll = false;\n\n    for (const patternOrAlias of [pattern, ...aliases]) {\n      const {matcher, generator, wrapperGenerator, isCatchAll} = parsePattern(patternOrAlias);\n      this._patterns.push({pattern: patternOrAlias, matcher, generator, wrapperGenerator});\n\n      if (isCatchAll) {\n        this._isCatchAll = true;\n      }\n    }\n\n    if (this._isCatchAll && this._patterns.length > 1) {\n      throw new Error(\n        `Couldn't create the addressable '${name}' (a catch-all addressable cannot have aliases)`\n      );\n    }\n\n    this._filter = filter;\n    this._transformers = transformers;\n  }\n\n  /**\n   * Returns the name of the addressable.\n   *\n   * @returns A string.\n   *\n   * @example\n   * ```\n   * const addressable = new Addressable('View', '/');\n   *\n   * addressable.getName(); // => 'View'\n   * ```\n   *\n   * @category Basic Methods\n   */\n  getName() {\n    return this._name;\n  }\n\n  /**\n   * Returns the canonical URL pattern of the addressable.\n   *\n   * @returns An [URL pattern](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type) string.\n   *\n   * @example\n   * ```\n   * const addressable = new Addressable('View', '/movies/:slug', {aliases: ['/films/:slug']});\n   *\n   * addressable.getPattern(); // => '/movies/:slug'\n   * ```\n   *\n   * @category Basic Methods\n   */\n  getPattern() {\n    return this._patterns[0].pattern;\n  }\n\n  isCatchAll() {\n    return this._isCatchAll;\n  }\n\n  getParams() {\n    const params: Params = {};\n\n    for (const [name, descriptor] of Object.entries(this._params)) {\n      params[name] = descriptor.specifier;\n    }\n\n    return params;\n  }\n\n  /**\n   * Returns the URL pattern aliases of the addressable.\n   *\n   * @returns An array of [URL pattern](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type) strings.\n   *\n   * @example\n   * ```\n   * const addressable = new Addressable('View', '/', {aliases: ['/home']});\n   *\n   * addressable.getAliases(); // => ['/home']\n   * ```\n   *\n   * @category Basic Methods\n   */\n  getAliases() {\n    return this._patterns.slice(1).map(({pattern}) => pattern);\n  }\n\n  getFilter() {\n    return this._filter;\n  }\n\n  getTransformers() {\n    return this._transformers;\n  }\n\n  transformMethod(method: Function, request: any) {\n    const transformers = this._transformers;\n\n    if (isEmpty(transformers)) {\n      // OPTIMIZATION\n      return method;\n    }\n\n    return function (this: any, params: any) {\n      if (transformers.input !== undefined) {\n        params = transformers.input.call(this, params, request);\n      }\n\n      return possiblyAsync.invoke(\n        () => method.call(this, params),\n        (result) => {\n          if (transformers.output !== undefined) {\n            result = transformers.output.call(this, result, request);\n          }\n\n          return result;\n        },\n        (error) => {\n          if (transformers.error !== undefined) {\n            return transformers.error.call(this, error, request);\n          }\n\n          throw error;\n        }\n      );\n    };\n  }\n\n  /**\n   * Checks if the addressable matches the specified URL.\n   *\n   * @param url A string or a [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) object.\n   *\n   * @returns If the addressable matches the specified URL, a plain object containing the identifiers and parameters included in the URL is returned. Otherwise, `undefined` is returned.\n   *\n   * @example\n   * ```\n   * const addressable = new Addressable('View', '/movies/:slug', {\n   *  params: {showDetails: 'boolean?'}\n   * });\n   *\n   * addressable.matchURL('/movies/abc123');\n   * // => {identifiers: {slug: 'abc123'}, parameters: {showDetails: undefined}}\n   *\n   * addressable.matchURL('/movies/abc123?showDetails=1');\n   * // => {identifiers: {slug: 'abc123'}, parameters: {showDetails: true}}\n   *\n   * addressable.matchURL('/films'); // => undefined\n   * ```\n   *\n   * @category URL Matching and Generation\n   */\n  matchURL(url: URL | string, request?: any) {\n    const {pathname: path, search: queryString} = normalizeURL(url);\n\n    const result = this.matchPath(path, request);\n\n    if (result !== undefined) {\n      const query: Record<string, string> = parseQuery(queryString);\n      const params: Record<string, any> = {};\n\n      for (const [name, descriptor] of Object.entries(this._params)) {\n        const queryValue = query[name];\n        const paramValue = deserializeParam(name, queryValue, descriptor);\n        params[name] = paramValue;\n      }\n\n      return {params, ...result};\n    }\n\n    return undefined;\n  }\n\n  matchPath(path: string, request?: any) {\n    for (const {matcher, wrapperGenerator} of this._patterns) {\n      const identifiers = matcher(path);\n\n      if (identifiers === undefined) {\n        continue;\n      }\n\n      if (this._filter !== undefined && !this._filter(request)) {\n        continue;\n      }\n\n      const wrapperPath = wrapperGenerator(identifiers);\n\n      return {identifiers, wrapperPath};\n    }\n\n    return undefined;\n  }\n\n  /**\n   * Generates an URL for the addressable.\n   *\n   * @param [identifiers] An optional object containing the identifiers to include in the generated URL.\n   * @param [params] An optional object containing the parameters to include in the generated URL.\n   * @param [options.hash] An optional string specifying a hash (i.e., a [fragment identifier](https://en.wikipedia.org/wiki/URI_fragment)) to include in the generated URL.\n   *\n   * @returns A string.\n   *\n   * @example\n   * ```\n   * const addressable = new Addressable('View', '/movies/:slug', {\n   *  params: {showDetails: 'boolean?'}\n   * });\n   *\n   * addressable.generateURL({slug: 'abc123'}); // => '/movies/abc123'\n   *\n   * addressable.generateURL({slug: 'abc123'}, {showDetails: true});\n   * // => '/movies/abc123?showDetails=1'\n   *\n   * addressable.generateURL({slug: 'abc123'}, {showDetails: true}, {hash: 'actors'});\n   * // => '/movies/abc123?showDetails=1#actors'\n   *\n   * addressable.generateURL({}); // => Error (the slug parameter is mandatory)\n   * ```\n   *\n   * @category URL Matching and Generation\n   */\n  generateURL(\n    identifiers?: Record<string, any>,\n    params?: Record<string, any>,\n    options?: URLOptions\n  ) {\n    let url = this.generatePath(identifiers);\n\n    const queryString = this.generateQueryString(params);\n\n    if (queryString !== '') {\n      url += `?${queryString}`;\n    }\n\n    if (options?.hash) {\n      url += `#${options.hash}`;\n    }\n\n    return url;\n  }\n\n  generatePath(identifiers?: Record<string, any>) {\n    return this._patterns[0].generator(identifiers);\n  }\n\n  generateQueryString(params: Record<string, any> = {}) {\n    const query: Record<string, string | undefined> = {};\n\n    for (const [name, descriptor] of Object.entries(this._params)) {\n      const paramValue = params[name];\n      const queryValue = serializeParam(name, paramValue, descriptor);\n      query[name] = queryValue;\n    }\n\n    return stringifyQuery(query);\n  }\n\n  /**\n   * @typedef URLPattern\n   *\n   * A string defining the canonical URL pattern (or an URL pattern alias) of an addressable.\n   *\n   * An URL pattern is composed of a *route pattern* (e.g., `'/movies'`) and can be prefixed with a *wrapper pattern*, which should be enclosed with square brackets (e.g., `'[/admin]'`).\n   *\n   * *Route patterns* and *wrapper patterns* can be composed of several *segments* separated by slashes (e.g., `'/movies/top-50'` or `'[/admin/movies]'`).\n   *\n   * A *segment* can be an arbitrary string (e.g., `'movies'`) or the name of a [component identifier attribute](https://layrjs.com/docs/v2/reference/identifier-attribute) (e.g., `'id'`) prefixed with a colon sign (`':'`). Note that a component identifier attribute can reference an identifier attribute of a related component (e.g., `'collection.id'`).\n   *\n   * Optionally, an URL pattern can be suffixed with wildcard character (`'*'`) to represent a catch-all URL.\n   *\n   * **Examples:**\n   *\n   * - `'/'`: Root URL pattern.\n   * - `'/movies'`: URL pattern without identifier attributes.\n   * - `'/movies/:id'`: URL pattern with one identifier attribute (`id`).\n   * - `'/collections/:collection.id/movies/:id'`: URL pattern with two identifier attributes (`collection.id` and `id`).\n   * - `[/]movies`: URL pattern composed of a wrapper pattern (`'[/]'`) and a route pattern (`'movies'`).\n   * - `'[/collections/:collection.id]/movies/:id'`: URL pattern composed of a wrapper pattern (`'[/collections/:collection.id]'`), a route pattern (`'/movies/:id'`), and two identifier attributes (`collection.id` and `id`).\n   * - `'/*'`: URL pattern that can match any URL. It can be helpful to display, for example, a \"Not Found\" page.\n   *\n   * @category Types\n   */\n\n  /**\n   * @typedef URLParameters\n   *\n   * An object defining the URL parameters of an addressable.\n   *\n   * The object can contain some pairs of `name` and `type` where `name` should be an arbitrary string representing the name of an URL parameter and `type` should be a string representing its type.\n   *\n   * Currently, `type` can be one of the following strings:\n   *\n   * - `'boolean'`\n   * - `'number'`\n   * - `'string'`\n   * - `'Date'`\n   *\n   * Optionally, `type` can be suffixed with a question mark (`'?'`) to specify an optional URL parameter.\n   *\n   * **Examples:**\n   *\n   * - `{step: 'number'}\n   * - `{showDetails: 'boolean?'}`\n   * - `{page: 'number?', orderBy: 'string?'}`\n   *\n   * @category Types\n   */\n\n  static isAddressable(value: any): value is Addressable {\n    return isAddressableInstance(value);\n  }\n}\n\n/**\n * Returns whether the specified value is an [`Addressable`](https://layrjs.com/docs/v2/reference/addressable) class.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isAddressableClass(value: any): value is typeof Addressable {\n  return typeof value?.isAddressable === 'function';\n}\n\n/**\n * Returns whether the specified value is an [`Addressable`](https://layrjs.com/docs/v2/reference/addressable) instance.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isAddressableInstance(value: any): value is Addressable {\n  return typeof value?.constructor?.isAddressable === 'function';\n}\n"
  },
  {
    "path": "packages/routable/src/decorators.test.ts",
    "content": "import {Component, provide, primaryIdentifier, attribute} from '@layr/component';\nimport createError from 'http-errors';\n\nimport {Routable, callRouteByURL} from './routable';\nimport {route, wrapper, httpRoute} from './decorators';\nimport {isRouteInstance} from './route';\nimport {isWrapperInstance} from './wrapper';\n\ndescribe('Decorators', () => {\n  test('@route()', async () => {\n    class Studio extends Component {\n      @primaryIdentifier() id!: string;\n    }\n\n    class Movie extends Routable(Component) {\n      @provide() static Studio = Studio;\n\n      @primaryIdentifier() id!: string;\n\n      @attribute('Studio') studio!: Studio;\n\n      @route('/movies', {aliases: ['/films']}) static ListPage() {\n        return `All movies`;\n      }\n\n      // Use a getter to simulate the view() decorator\n      @route('/movies/:id', {aliases: ['/films/:id']}) get ItemPage() {\n        return function (this: Movie) {\n          return `Movie #${this.id}`;\n        };\n      }\n\n      // Use a getter to simulate the view() decorator\n      @route('/studios/:studio.id/movies/:id') get ItemWithStudioPage() {\n        return function (this: Movie) {\n          return `Movie #${this.id}`;\n        };\n      }\n    }\n\n    // --- Class routes ---\n\n    const listPageRoute = Movie.getRoute('ListPage');\n\n    expect(isRouteInstance(listPageRoute)).toBe(true);\n    expect(listPageRoute.getName()).toBe('ListPage');\n    expect(listPageRoute.getPattern()).toBe('/movies');\n    expect(listPageRoute.getAliases()).toEqual(['/films']);\n    expect(listPageRoute.matchURL('/movies')).toStrictEqual({\n      identifiers: {},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(listPageRoute.matchURL('/films')).toStrictEqual({\n      identifiers: {},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(listPageRoute.generateURL()).toBe('/movies');\n\n    expect(Movie.ListPage.matchURL('/movies')).toStrictEqual({\n      identifiers: {},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(Movie.ListPage.matchURL('/films')).toStrictEqual({\n      identifiers: {},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(Movie.ListPage.generateURL()).toBe('/movies');\n\n    // --- Prototype routes ---\n\n    const itemPageRoute = Movie.prototype.getRoute('ItemPage');\n\n    expect(isRouteInstance(itemPageRoute)).toBe(true);\n    expect(itemPageRoute.getName()).toBe('ItemPage');\n    expect(itemPageRoute.getPattern()).toBe('/movies/:id');\n    expect(itemPageRoute.getAliases()).toEqual(['/films/:id']);\n    expect(itemPageRoute.matchURL('/movies/abc123')).toStrictEqual({\n      identifiers: {id: 'abc123'},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(itemPageRoute.matchURL('/films/abc123')).toStrictEqual({\n      identifiers: {id: 'abc123'},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(itemPageRoute.generateURL({id: 'abc123'})).toBe('/movies/abc123');\n\n    expect(Movie.prototype.ItemPage.matchURL('/movies/abc123')).toStrictEqual({\n      identifiers: {id: 'abc123'},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(Movie.prototype.ItemPage.matchURL('/films/abc123')).toStrictEqual({\n      identifiers: {id: 'abc123'},\n      params: {},\n      wrapperPath: undefined\n    });\n\n    const itemWithStudioPageRoute = Movie.prototype.getRoute('ItemWithStudioPage');\n    expect(itemWithStudioPageRoute.getName()).toBe('ItemWithStudioPage');\n    expect(itemWithStudioPageRoute.getPattern()).toBe('/studios/:studio.id/movies/:id');\n    expect(itemWithStudioPageRoute.getAliases()).toEqual([]);\n    expect(itemWithStudioPageRoute.matchURL('/studios/abc/movies/123')).toStrictEqual({\n      identifiers: {id: '123', studio: {id: 'abc'}},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(itemWithStudioPageRoute.generateURL({id: '123', studio: {id: 'abc'}})).toBe(\n      '/studios/abc/movies/123'\n    );\n\n    expect(Movie.prototype.ItemWithStudioPage.matchURL('/studios/abc/movies/123')).toStrictEqual({\n      identifiers: {id: '123', studio: {id: 'abc'}},\n      params: {},\n      wrapperPath: undefined\n    });\n\n    // --- Instance routes ---\n\n    const studio = new Studio({id: 'abc'});\n    const movie = new Movie({id: '123', studio});\n\n    expect(movie.ItemPage.generateURL()).toBe('/movies/123');\n    expect(movie.ItemWithStudioPage.generateURL()).toBe('/studios/abc/movies/123');\n  });\n\n  test('@wrapper()', async () => {\n    class Movie extends Routable(Component) {\n      @wrapper('/movies') static MainLayout() {}\n\n      @wrapper('[/movies]/:id') ItemLayout() {}\n    }\n\n    // --- Class wrappers ---\n\n    const mainLayoutWrapper = Movie.getWrapper('MainLayout');\n\n    expect(isWrapperInstance(mainLayoutWrapper)).toBe(true);\n    expect(mainLayoutWrapper.getName()).toBe('MainLayout');\n    expect(mainLayoutWrapper.getPattern()).toBe('/movies');\n\n    // --- Prototype wrappers ---\n\n    const itemLayoutWrapper = Movie.prototype.getWrapper('ItemLayout');\n\n    expect(isWrapperInstance(itemLayoutWrapper)).toBe(true);\n    expect(itemLayoutWrapper.getName()).toBe('ItemLayout');\n    expect(itemLayoutWrapper.getPattern()).toBe('[/movies]/:id');\n  });\n\n  test('@httpRoute()', async () => {\n    class Movie extends Routable(Component) {\n      @primaryIdentifier() id!: string;\n\n      @httpRoute('GET', '/movies/:id') async getMovie() {\n        if (this.id === 'abc123') {\n          return {id: 'abc123', title: 'Inception'};\n        }\n\n        throw createError(404, 'Movie not found', {\n          displayMessage: \"Couldn't find a movie with the specified 'id'\",\n          code: 'ITEM_NOT_FOUND'\n        });\n      }\n\n      @httpRoute('*', '/*') static routeNotFound() {\n        throw createError(404, 'Route not found');\n      }\n    }\n\n    expect(await callRouteByURL(Movie, '/movies/abc123', {method: 'GET'})).toStrictEqual({\n      status: 200,\n      headers: {'content-type': 'application/json'},\n      body: JSON.stringify({id: 'abc123', title: 'Inception'})\n    });\n\n    expect(await callRouteByURL(Movie, '/movies/def456', {method: 'GET'})).toStrictEqual({\n      status: 404,\n      headers: {'content-type': 'application/json'},\n      body: JSON.stringify({\n        message: 'Movie not found',\n        displayMessage: \"Couldn't find a movie with the specified 'id'\",\n        code: 'ITEM_NOT_FOUND'\n      })\n    });\n\n    expect(await callRouteByURL(Movie, '/films', {method: 'GET'})).toStrictEqual({\n      status: 404,\n      headers: {'content-type': 'application/json'},\n      body: JSON.stringify({\n        message: 'Route not found'\n      })\n    });\n\n    expect(() => {\n      // @ts-ignore\n      class Movie extends Routable(Component) {\n        // @ts-expect-error\n        @httpRoute('TRACE', '/trace') static trace() {}\n      }\n    }).toThrow(\n      \"The HTTP method 'TRACE' is not supported by the @httpRoute() decorator (supported values are: 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', or '*')\"\n    );\n  });\n});\n"
  },
  {
    "path": "packages/routable/src/decorators.ts",
    "content": "import {Navigator, URLOptions, NavigationOptions} from '@layr/navigator';\nimport {hasOwnProperty} from 'core-helpers';\n\nimport type {RoutableComponent} from './routable';\nimport type {Route, RouteOptions} from './route';\nimport type {WrapperOptions} from './wrapper';\nimport type {Pattern} from './pattern';\nimport {isRoutableClassOrInstance} from './utilities';\n\n/**\n * Defines a [route](https://layrjs.com/docs/v2/reference/route) for a static or instance method in a [routable component](https://layrjs.com/docs/v2/reference/routable#routable-component-class).\n *\n * @param pattern The canonical [URL pattern](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type) of the route.\n * @param [options] An object specifying the options to pass to the `Route`'s [constructor](https://layrjs.com/docs/v2/reference/addressable#constructor) when the route is created.\n *\n * @details\n * **Shortcut functions:**\n *\n * In addition to defining a route, the decorator adds some shortcut functions to the decorated method so that you can interact with the route more easily.\n *\n * For example, if you define a `route` for a `View()` method you automatically get the following functions:\n *\n * - `View.matchURL(url)` is the equivalent of [`route.matchURL(url)`](https://layrjs.com/docs/v2/reference/addressable#match-url-instance-method).\n * - `View.generateURL([params], [options])` is the equivalent of [`route.generateURL(component, [params], [options])`](https://layrjs.com/docs/v2/reference/addressable#generate-url-instance-method) where `component` is the [routable component](https://layrjs.com/docs/v2/reference/routable#routable-component-class) associated with the `View()` method.\n *\n * If the defined `route` is controlled by a [`navigator`](https://layrjs.com/docs/v2/reference/navigator), you also get the following shortcut functions:\n *\n * - `View.navigate([params], [options])` is the equivalent of [`navigator.navigate(url, options)`](https://layrjs.com/docs/v2/reference/navigator#navigate-instance-method) where `url` is generated by calling `View.generateURL([params], [options])`.\n * - `View.redirect([params], [options])` is the equivalent of [`navigator.redirect(url, options)`](https://layrjs.com/docs/v2/reference/navigator#redirect-instance-method) where `url` is generated by calling `View.generateURL([params], [options])`.\n * - `View.reload([params], [options])` is the equivalent of [`navigator.reload(url)`](https://layrjs.com/docs/v2/reference/navigator#reload-instance-method) where `url` is generated by calling `View.generateURL([params], [options])`.\n * - `View.isActive()` returns a boolean indicating whether the `route`'s URL (generated by calling `View.generateURL()`) matches the current `navigator`'s URL.\n *\n * Lastly, if the defined `route` is controlled by a [`navigator`](https://layrjs.com/docs/v2/reference/navigator) that is created by using the [`useBrowserNavigator()`](https://layrjs.com/docs/v2/reference/react-integration#use-browser-navigator-react-hook) React hook, you also get the following shortcut React component:\n *\n * - `View.Link({params, hash, ...props})` is the equivalent of [`navigator.Link({to, ...props})`](https://layrjs.com/docs/v2/reference/browser-navigator#link-instance-method) where `to` is generated by calling `View.generateURL(params, {hash})`.\n *\n * @examplelink See an example of use in the [`BrowserNavigatorView`](https://layrjs.com/docs/v2/reference/react-integration#browser-navigator-view-react-component) React component.\n *\n * @category Decorators\n * @decorator\n */\nexport function route(pattern: Pattern, options: RouteOptions = {}) {\n  return function (\n    target: typeof RoutableComponent | RoutableComponent,\n    name: string,\n    descriptor: PropertyDescriptor\n  ) {\n    const {value: method, get: originalGet, configurable, enumerable} = descriptor;\n\n    if (\n      !(\n        isRoutableClassOrInstance(target) &&\n        (typeof method === 'function' || originalGet !== undefined) &&\n        enumerable === false\n      )\n    ) {\n      throw new Error(\n        `@route() should be used to decorate a routable component method (property: '${name}')`\n      );\n    }\n\n    const route: Route = target.setRoute(name, pattern, options);\n\n    const decorate = function (\n      this: typeof RoutableComponent | RoutableComponent,\n      method: Function\n    ) {\n      const component = this;\n\n      defineMethod(method, 'matchURL', function (url: URL | string) {\n        return route.matchURL(url);\n      });\n\n      defineMethod(method, 'generateURL', function (params?: any, options?: URLOptions) {\n        return route.generateURL(component, params, options);\n      });\n\n      defineMethod(method, 'generatePath', function () {\n        return route.generatePath(component);\n      });\n\n      defineMethod(method, 'generateQueryString', function (params?: any) {\n        return route.generateQueryString(params);\n      });\n\n      Object.defineProperty(method, '__isDecorated', {value: true});\n    };\n\n    const decorateWithNavigator = function (\n      this: typeof RoutableComponent | RoutableComponent,\n      method: Function,\n      navigator: Navigator\n    ) {\n      defineMethod(\n        method,\n        'navigate',\n        function (this: Function, params?: any, options?: URLOptions & NavigationOptions) {\n          return navigator.navigate(this.generateURL(params, options), options);\n        }\n      );\n\n      defineMethod(\n        method,\n        'redirect',\n        function (this: Function, params?: any, options?: URLOptions & NavigationOptions) {\n          return navigator.redirect(this.generateURL(params, options), options);\n        }\n      );\n\n      defineMethod(method, 'reload', function (this: Function, params?: any, options?: URLOptions) {\n        navigator.reload(this.generateURL(params, options));\n      });\n\n      defineMethod(method, 'isActive', function (this: Function) {\n        return this.matchURL(navigator.getCurrentURL()) !== undefined;\n      });\n\n      navigator.applyCustomRouteDecorators(this, method);\n\n      Object.defineProperty(method, '__isDecoratedWithNavigator', {value: true});\n    };\n\n    const defineMethod = function (object: any, name: string, func: Function) {\n      Object.defineProperty(object, name, {\n        value: func,\n        writable: true,\n        enumerable: false,\n        configurable: true\n      });\n    };\n\n    const get = function (this: typeof RoutableComponent | RoutableComponent) {\n      // TODO: Don't assume that `originalGet` returns a bound method (like when @view() is used).\n      // We should return a bound method in any case to make instance routes work\n      // (see `decorator.test.ts`)\n\n      const actualMethod = originalGet !== undefined ? originalGet.call(this) : method;\n\n      if (typeof actualMethod !== 'function') {\n        throw new Error(`@route() can only be used on methods`);\n      }\n\n      if (!hasOwnProperty(actualMethod, '__isDecorated')) {\n        decorate.call(this, actualMethod);\n      }\n\n      if (!hasOwnProperty(actualMethod, '__isDecoratedWithNavigator')) {\n        const navigator = this.findNavigator();\n\n        if (navigator !== undefined) {\n          decorateWithNavigator.call(this, actualMethod, navigator);\n        }\n      }\n\n      return actualMethod;\n    };\n\n    return {get, configurable, enumerable};\n  };\n}\n\n/**\n * Defines a [wrapper](https://layrjs.com/docs/v2/reference/wrapper) for a static or instance method in a [routable component](https://layrjs.com/docs/v2/reference/routable#routable-component-class).\n *\n * @param pattern The canonical [URL pattern](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type) of the wrapper.\n * @param [options] An object specifying the options to pass to the `Wrapper`'s [constructor](https://layrjs.com/docs/v2/reference/addressable#constructor) when the wrapper is created.\n *\n * @examplelink See an example of use in the [`BrowserNavigatorView`](https://layrjs.com/docs/v2/reference/react-integration#browser-navigator-view-react-component) React component.\n *\n * @category Decorators\n * @decorator\n */\nexport function wrapper(pattern: Pattern, options: WrapperOptions = {}) {\n  return function (\n    target: typeof RoutableComponent | RoutableComponent,\n    name: string,\n    descriptor: PropertyDescriptor\n  ) {\n    const {value: method, get, enumerable} = descriptor;\n\n    if (\n      !(\n        isRoutableClassOrInstance(target) &&\n        (typeof method === 'function' || get !== undefined) &&\n        enumerable === false\n      )\n    ) {\n      throw new Error(\n        `@wrapper() should be used to decorate a routable component method (property: '${name}')`\n      );\n    }\n\n    target.setWrapper(name, pattern, options);\n\n    return descriptor;\n  };\n}\n\nconst HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] as const;\nconst ANY_HTTP_METHOD = '*';\n\ntype HTTPMethod = typeof HTTP_METHODS[number];\ntype AnyHTTPMethod = typeof ANY_HTTP_METHOD;\n\nexport function basicHTTPRouteInputTransformer(params: any, _request: any) {\n  return params;\n}\n\nexport function basicHTTPRouteOutputTransformer(result: any, request: any) {\n  let response: {status?: number; headers?: Record<string, string>; body?: string | Buffer} = {};\n\n  if (result === undefined) {\n    response.status = 204; // No Content\n  } else {\n    if (request?.method === 'POST') {\n      response.status = 201; // Created\n    } else {\n      response.status = 200; // OK\n    }\n\n    if (Buffer.isBuffer(result)) {\n      response.headers = {'content-type': 'application/octet-stream'};\n      response.body = result;\n    } else {\n      response.headers = {'content-type': 'application/json'};\n      response.body = JSON.stringify(result);\n    }\n  }\n\n  return response;\n}\n\nexport function basicHTTPRouteErrorTransformer(error: any, _request: any) {\n  const response: {status: number; headers: Record<string, string>; body: string} = {\n    status: 500,\n    headers: {'content-type': 'application/json'},\n    body: JSON.stringify({\n      message: 'Internal server error'\n    })\n  };\n\n  if (typeof error !== 'object' || error === null) {\n    return response;\n  }\n\n  if (typeof error.status === 'number') {\n    response.status = error.status;\n  }\n\n  const expose: boolean = typeof error.expose === 'boolean' ? error.expose : response.status < 500;\n\n  if (expose) {\n    const body: Record<string, any> = {};\n\n    body.message = typeof error.message === 'string' ? error.message : 'Unknown error';\n\n    for (const [key, value] of Object.entries(error)) {\n      if (key === 'status' || key === 'message' || value === undefined) {\n        continue;\n      }\n\n      body[key] = value;\n    }\n\n    response.body = JSON.stringify(body);\n  }\n\n  return response;\n}\n\nexport function httpRoute(\n  httpMethod: HTTPMethod | AnyHTTPMethod,\n  pattern: Pattern,\n  options: RouteOptions = {}\n) {\n  if (!(httpMethod === ANY_HTTP_METHOD || HTTP_METHODS.includes(httpMethod))) {\n    throw new Error(\n      `The HTTP method '${httpMethod}' is not supported by the @httpRoute() decorator (supported values are: ${HTTP_METHODS.map(\n        (method) => `'${method}'`\n      ).join(', ')}, or '${ANY_HTTP_METHOD}')`\n    );\n  }\n\n  const {\n    filter = function (request) {\n      return httpMethod === '*'\n        ? HTTP_METHODS.includes(request?.method)\n        : request?.method === httpMethod;\n    },\n    transformers = {},\n    ...otherOptions\n  } = options;\n\n  const {\n    input = basicHTTPRouteInputTransformer,\n    output = basicHTTPRouteOutputTransformer,\n    error = basicHTTPRouteErrorTransformer\n  } = transformers;\n\n  return function (\n    target: typeof RoutableComponent | RoutableComponent,\n    name: string,\n    descriptor: PropertyDescriptor\n  ) {\n    return route(pattern, {filter, transformers: {input, output, error}, ...otherOptions})(\n      target,\n      name,\n      descriptor\n    );\n  };\n}\n"
  },
  {
    "path": "packages/routable/src/index.ts",
    "content": "export * from './addressable';\nexport * from './decorators';\nexport * from './param';\nexport * from './pattern';\nexport * from './routable';\nexport * from './route';\nexport * from './utilities';\nexport * from './wrapper';\n"
  },
  {
    "path": "packages/routable/src/js-tests/decorators.test.js",
    "content": "import {Component, provide, primaryIdentifier, attribute} from '@layr/component';\nimport createError from 'http-errors';\n\nimport {Routable, callRouteByURL} from '../routable';\nimport {route, wrapper, httpRoute} from '../decorators';\nimport {isRouteInstance} from '../route';\nimport {isWrapperInstance} from '../wrapper';\n\ndescribe('Decorators', () => {\n  test('@route()', async () => {\n    class Studio extends Component {\n      @primaryIdentifier() id;\n    }\n\n    class Movie extends Routable(Component) {\n      @provide() static Studio = Studio;\n\n      @primaryIdentifier() id;\n\n      @attribute('Studio') studio;\n\n      @route('/movies', {aliases: ['/films']}) static ListPage() {\n        return `All movies`;\n      }\n\n      // Use a getter to simulate the view() decorator\n      @route('/movies/:id', {aliases: ['/films/:id']}) get ItemPage() {\n        return function () {\n          return `Movie #${this.id}`;\n        };\n      }\n\n      // Use a getter to simulate the view() decorator\n      @route('/studios/:studio.id/movies/:id') get ItemWithStudioPage() {\n        return function () {\n          return `Movie #${this.id}`;\n        };\n      }\n    }\n\n    // --- Class routes ---\n\n    const listPageRoute = Movie.getRoute('ListPage');\n\n    expect(isRouteInstance(listPageRoute)).toBe(true);\n    expect(listPageRoute.getName()).toBe('ListPage');\n    expect(listPageRoute.getPattern()).toBe('/movies');\n    expect(listPageRoute.getAliases()).toEqual(['/films']);\n    expect(listPageRoute.matchURL('/movies')).toStrictEqual({\n      identifiers: {},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(listPageRoute.matchURL('/films')).toStrictEqual({\n      identifiers: {},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(listPageRoute.generateURL()).toBe('/movies');\n\n    expect(Movie.ListPage.matchURL('/movies')).toStrictEqual({\n      identifiers: {},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(Movie.ListPage.matchURL('/films')).toStrictEqual({\n      identifiers: {},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(Movie.ListPage.generateURL()).toBe('/movies');\n\n    // --- Prototype routes ---\n\n    const itemPageRoute = Movie.prototype.getRoute('ItemPage');\n\n    expect(isRouteInstance(itemPageRoute)).toBe(true);\n    expect(itemPageRoute.getName()).toBe('ItemPage');\n    expect(itemPageRoute.getPattern()).toBe('/movies/:id');\n    expect(itemPageRoute.getAliases()).toEqual(['/films/:id']);\n    expect(itemPageRoute.matchURL('/movies/abc123')).toStrictEqual({\n      identifiers: {id: 'abc123'},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(itemPageRoute.matchURL('/films/abc123')).toStrictEqual({\n      identifiers: {id: 'abc123'},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(itemPageRoute.generateURL({id: 'abc123'})).toBe('/movies/abc123');\n\n    expect(Movie.prototype.ItemPage.matchURL('/movies/abc123')).toStrictEqual({\n      identifiers: {id: 'abc123'},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(Movie.prototype.ItemPage.matchURL('/films/abc123')).toStrictEqual({\n      identifiers: {id: 'abc123'},\n      params: {},\n      wrapperPath: undefined\n    });\n\n    const itemWithStudioPageRoute = Movie.prototype.getRoute('ItemWithStudioPage');\n    expect(itemWithStudioPageRoute.getName()).toBe('ItemWithStudioPage');\n    expect(itemWithStudioPageRoute.getPattern()).toBe('/studios/:studio.id/movies/:id');\n    expect(itemWithStudioPageRoute.getAliases()).toEqual([]);\n    expect(itemWithStudioPageRoute.matchURL('/studios/abc/movies/123')).toStrictEqual({\n      identifiers: {id: '123', studio: {id: 'abc'}},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(itemWithStudioPageRoute.generateURL({id: '123', studio: {id: 'abc'}})).toBe(\n      '/studios/abc/movies/123'\n    );\n\n    expect(Movie.prototype.ItemWithStudioPage.matchURL('/studios/abc/movies/123')).toStrictEqual({\n      identifiers: {id: '123', studio: {id: 'abc'}},\n      params: {},\n      wrapperPath: undefined\n    });\n\n    // --- Instance routes ---\n\n    const studio = new Studio({id: 'abc'});\n    const movie = new Movie({id: '123', studio});\n\n    expect(movie.ItemPage.generateURL()).toBe('/movies/123');\n    expect(movie.ItemWithStudioPage.generateURL()).toBe('/studios/abc/movies/123');\n  });\n\n  test('@wrapper()', async () => {\n    class Movie extends Routable(Component) {\n      @wrapper('/movies') static MainLayout() {}\n\n      @wrapper('[/movies]/:id') ItemLayout() {}\n    }\n\n    // --- Class wrappers ---\n\n    const mainLayoutWrapper = Movie.getWrapper('MainLayout');\n\n    expect(isWrapperInstance(mainLayoutWrapper)).toBe(true);\n    expect(mainLayoutWrapper.getName()).toBe('MainLayout');\n    expect(mainLayoutWrapper.getPattern()).toBe('/movies');\n\n    // --- Prototype wrappers ---\n\n    const itemLayoutWrapper = Movie.prototype.getWrapper('ItemLayout');\n\n    expect(isWrapperInstance(itemLayoutWrapper)).toBe(true);\n    expect(itemLayoutWrapper.getName()).toBe('ItemLayout');\n    expect(itemLayoutWrapper.getPattern()).toBe('[/movies]/:id');\n  });\n\n  test('@httpRoute()', async () => {\n    class Movie extends Routable(Component) {\n      @primaryIdentifier() id;\n\n      @httpRoute('GET', '/movies/:id') async getMovie() {\n        if (this.id === 'abc123') {\n          return {id: 'abc123', title: 'Inception'};\n        }\n\n        throw createError(404, 'Movie not found', {\n          displayMessage: \"Couldn't find a movie with the specified 'id'\",\n          code: 'ITEM_NOT_FOUND'\n        });\n      }\n\n      @httpRoute('*', '/*') static routeNotFound() {\n        throw createError(404, 'Route not found');\n      }\n    }\n\n    expect(await callRouteByURL(Movie, '/movies/abc123', {method: 'GET'})).toStrictEqual({\n      status: 200,\n      headers: {'content-type': 'application/json'},\n      body: JSON.stringify({id: 'abc123', title: 'Inception'})\n    });\n\n    expect(await callRouteByURL(Movie, '/movies/def456', {method: 'GET'})).toStrictEqual({\n      status: 404,\n      headers: {'content-type': 'application/json'},\n      body: JSON.stringify({\n        message: 'Movie not found',\n        displayMessage: \"Couldn't find a movie with the specified 'id'\",\n        code: 'ITEM_NOT_FOUND'\n      })\n    });\n\n    expect(await callRouteByURL(Movie, '/films', {method: 'GET'})).toStrictEqual({\n      status: 404,\n      headers: {'content-type': 'application/json'},\n      body: JSON.stringify({\n        message: 'Route not found'\n      })\n    });\n\n    expect(() => {\n      class Movie extends Routable(Component) {\n        // @ts-expect-error\n        @httpRoute('TRACE', '/trace') static trace() {}\n      }\n    }).toThrow(\n      \"The HTTP method 'TRACE' is not supported by the @httpRoute() decorator (supported values are: 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', or '*')\"\n    );\n  });\n});\n"
  },
  {
    "path": "packages/routable/src/js-tests/jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"module\": \"CommonJS\",\n    \"checkJs\": true,\n    \"experimentalDecorators\": true\n  }\n}\n"
  },
  {
    "path": "packages/routable/src/param.ts",
    "content": "import {getTypeOf} from 'core-helpers';\n\nexport type Params = Record<string, TypeSpecifier>;\n\nconst PARAM_TYPE = ['boolean', 'number', 'string', 'Date'] as const;\n\nexport type ParamType = typeof PARAM_TYPE[number];\n\nexport type TypeSpecifier = `${ParamType}${'' | '?'}`;\n\nexport type ParamTypeDescriptor = {\n  type: ParamType;\n  isOptional: boolean;\n  specifier: TypeSpecifier;\n};\n\nexport function serializeParam(name: string, value: any, typeDescriptor: ParamTypeDescriptor) {\n  const {type, isOptional, specifier} = typeDescriptor;\n\n  if (value === undefined) {\n    if (isOptional) {\n      return undefined;\n    }\n\n    throw new Error(\n      `A required route (or wrapper) parameter is missing (name: '${name}', type: '${specifier}')`\n    );\n  }\n\n  if (type === 'boolean') {\n    if (typeof value === 'boolean') {\n      return value ? '1' : '0';\n    }\n  }\n\n  if (type === 'number') {\n    if (typeof value === 'number' && !isNaN(value)) {\n      return String(value);\n    }\n  }\n\n  if (type === 'string') {\n    if (typeof value === 'string') {\n      return value;\n    }\n  }\n\n  if (type === 'Date') {\n    if (value instanceof Date && !isNaN(value.valueOf())) {\n      return value.toISOString();\n    }\n  }\n\n  throw new Error(\n    `Couldn't serialize a route (or wrapper) parameter (name: '${name}', value: '${value}', expected type: '${specifier}', received type: '${getTypeOf(\n      value\n    )}')`\n  );\n}\n\nexport function deserializeParam(\n  name: string,\n  value: string | undefined,\n  typeDescriptor: ParamTypeDescriptor\n) {\n  const {type, isOptional, specifier} = typeDescriptor;\n\n  if (value === undefined) {\n    if (isOptional) {\n      return undefined;\n    }\n\n    throw new Error(\n      `A required route (or wrapper) parameter is missing (name: '${name}', type: '${specifier}')`\n    );\n  }\n\n  if (type === 'boolean') {\n    if (value === '0') {\n      return false;\n    }\n\n    if (value === '1') {\n      return true;\n    }\n  }\n\n  if (type === 'number') {\n    const result = Number(value);\n\n    if (!isNaN(result)) {\n      return result;\n    }\n  }\n\n  if (type === 'string') {\n    return value;\n  }\n\n  if (type === 'Date') {\n    const result = new Date(value);\n\n    if (!isNaN(result.valueOf())) {\n      return result;\n    }\n  }\n\n  throw new Error(\n    `Couldn't deserialize a route (or wrapper) parameter (name: '${name}', value: '${value}', type: '${specifier}')`\n  );\n}\n\nexport function parseParamTypeSpecifier(typeSpecifier: TypeSpecifier) {\n  let type: ParamType;\n  let isOptional: boolean;\n\n  if (typeof typeSpecifier !== 'string') {\n    throw new Error(\n      `Couldn't parse a route (or wrapper) parameter type (expected a string, but received a value of type '${getTypeOf(\n        typeSpecifier\n      )}')`\n    );\n  }\n\n  if (typeSpecifier.endsWith('?')) {\n    type = typeSpecifier.slice(0, -1) as ParamType;\n    isOptional = true;\n  } else {\n    type = typeSpecifier as ParamType;\n    isOptional = false;\n  }\n\n  if (type.length === 0) {\n    throw new Error(\n      \"Couldn't parse a route (or wrapper) parameter type (received an empty string)\"\n    );\n  }\n\n  if (!PARAM_TYPE.includes(type)) {\n    throw new Error(\n      `Couldn't parse a route (or wrapper) parameter type ('${type}' is not a supported type)`\n    );\n  }\n\n  return {type, isOptional};\n}\n"
  },
  {
    "path": "packages/routable/src/pattern.ts",
    "content": "import get from 'lodash/get';\nimport set from 'lodash/set';\nimport escapeRegExp from 'lodash/escapeRegExp';\n\nexport type Pattern = string;\n\nexport type PathMatcher = (path: string) => Record<string, any> | undefined;\nexport type PathGenerator = (identifiers?: Record<string, any>) => string | undefined;\n\nconst PATH_CHARACTER_REGEXP = /[A-Za-z0-9.\\-_@/]/;\nconst IDENTIFIER_NAME_CHARACTER_REGEXP = /[A-Za-z0-9.]/;\nconst IDENTIFIER_VALUE_GROUP_PATTERN = '([^/]+)';\n\nexport function parsePattern(pattern: Pattern) {\n  const {wrapperParts, regularParts, isCatchAll} = splitPattern(pattern);\n  const allParts = [...wrapperParts, ...regularParts];\n\n  const regExpParts: string[] = [];\n  const identifierNames: string[] = [];\n\n  for (const part of allParts) {\n    if (part.length === 0) {\n      continue;\n    }\n\n    if (part.startsWith(':')) {\n      const name = part.slice(1);\n      identifierNames.push(name);\n      regExpParts.push(IDENTIFIER_VALUE_GROUP_PATTERN);\n    } else {\n      regExpParts.push(escapeRegExp(part));\n    }\n  }\n\n  if (isCatchAll) {\n    regExpParts.push('.*');\n  }\n\n  const regExp = new RegExp('^' + regExpParts.join('') + '$');\n\n  const matcher: PathMatcher = function (path) {\n    const matches = path.match(regExp);\n\n    if (matches === null) {\n      return undefined;\n    }\n\n    const identifiers: Record<string, any> = {};\n\n    for (let index = 0; index < identifierNames.length; index++) {\n      const name = identifierNames[index];\n      const value = decodeURIComponent(matches[index + 1]);\n      set(identifiers, name, value);\n    }\n\n    return identifiers;\n  };\n\n  const generate = function (parts: string[], identifiers = {}) {\n    if (parts.length === 0) {\n      return undefined;\n    }\n\n    let path = '';\n\n    for (const part of parts) {\n      if (part.startsWith(':')) {\n        const name = part.slice(1);\n        const value = get(identifiers, name);\n        if (value === undefined || value === '') {\n          throw new Error(\n            `Couldn't build a route (or wrapper) path from the pattern '${pattern}' because the identifier '${name}' is missing`\n          );\n        }\n        path += encodeURIComponent(value);\n      } else {\n        path += part;\n      }\n    }\n\n    return path;\n  };\n\n  const generator: PathGenerator = function (identifiers) {\n    return generate(allParts, identifiers);\n  };\n\n  const wrapperGenerator: PathGenerator = function (identifiers) {\n    return generate(wrapperParts, identifiers);\n  };\n\n  return {matcher, generator, wrapperGenerator, isCatchAll};\n}\n\nfunction splitPattern(pattern: Pattern) {\n  let normalizedPattern = pattern.trim();\n\n  let isCatchAll: boolean;\n\n  const wildcardIndex = normalizedPattern.indexOf('*');\n\n  if (wildcardIndex !== -1) {\n    if (wildcardIndex !== normalizedPattern.length - 1) {\n      throw new Error(\n        `Couldn't parse the route (or wrapper) pattern '${pattern}' (a wildcard character '*' is only allowed at the end of the pattern)`\n      );\n    }\n    normalizedPattern = normalizedPattern.slice(0, -1);\n    isCatchAll = true;\n  } else {\n    isCatchAll = false;\n  }\n\n  const wrapperParts: string[] = [];\n  const regularParts: string[] = [];\n\n  let index = 0;\n  let partSection: 'wrapper' | 'regular' = 'regular';\n  let partType: 'path' | 'identifier' = 'path';\n  let partContent = '';\n\n  const flushPart = () => {\n    const parts = partSection === 'wrapper' ? wrapperParts : regularParts;\n\n    if (partType === 'path') {\n      parts.push(partContent);\n    } else {\n      if (partContent === '') {\n        throw new Error(\n          `Couldn't parse the route (or wrapper) pattern '${pattern}' (an identifier cannot be empty)`\n        );\n      }\n\n      parts.push(':' + partContent);\n    }\n\n    partContent = '';\n  };\n\n  while (index <= normalizedPattern.length - 1) {\n    const character = normalizedPattern[index++];\n\n    if (character === '[') {\n      if (partSection === 'regular') {\n        flushPart();\n        partSection = 'wrapper';\n        partType = 'path';\n      } else {\n        throw new Error(\n          `Couldn't parse the route (or wrapper) pattern '${pattern}' (encountered an unexpected opening wrapper delimiter '[')`\n        );\n      }\n    } else if (character === ']') {\n      if (partSection === 'wrapper') {\n        flushPart();\n        partSection = 'regular';\n        partType = 'path';\n      } else {\n        throw new Error(\n          `Couldn't parse the route (or wrapper) pattern '${pattern}' (encountered an unexpected closing wrapper delimiter ']')`\n        );\n      }\n    } else if (partType === 'path') {\n      if (PATH_CHARACTER_REGEXP.test(character)) {\n        partContent += character;\n      } else if (character === ':') {\n        flushPart();\n        partType = 'identifier';\n      } else {\n        throw new Error(\n          `Couldn't parse the route (or wrapper) pattern '${pattern}' (the character '${character}' cannot be used in a pattern)`\n        );\n      }\n    } else {\n      if (IDENTIFIER_NAME_CHARACTER_REGEXP.test(character)) {\n        partContent += character;\n      } else if (character !== ':') {\n        flushPart();\n        partContent = character;\n        partType = 'path';\n      } else {\n        throw new Error(\n          `Couldn't parse the route (or wrapper) pattern '${pattern}' (an identifier cannot be immediately followed by another identifier)`\n        );\n      }\n    }\n  }\n\n  flushPart();\n\n  if (partSection !== 'regular') {\n    throw new Error(\n      `Couldn't parse the route (or wrapper) pattern '${pattern}' (a closing wrapper delimiter ']' is missing)`\n    );\n  }\n\n  return {wrapperParts, regularParts, isCatchAll};\n}\n"
  },
  {
    "path": "packages/routable/src/routable.test.ts",
    "content": "import {Component, provide, primaryIdentifier, attribute} from '@layr/component';\n\nimport {Routable, callRouteByURL} from './routable';\nimport {isRoutableClass, isRoutableInstance} from './utilities';\n\ndescribe('Routable', () => {\n  test('Routable()', async () => {\n    class Movie extends Routable(Component) {}\n\n    expect(isRoutableClass(Movie)).toBe(true);\n    expect(isRoutableInstance(Movie.prototype)).toBe(true);\n  });\n\n  test('getRoute() and hasRoute()', async () => {\n    class Movie extends Routable(Component) {}\n\n    // --- Class routes ---\n\n    const listPageRoute = Movie.setRoute('ListPage', '/movies');\n\n    expect(Movie.getRoute('ListPage')).toBe(listPageRoute);\n\n    expect(Movie.hasRoute('HotPage')).toBe(false);\n    expect(() => Movie.getRoute('HotPage')).toThrow(\n      \"The route 'HotPage' is missing (component: 'Movie')\"\n    );\n\n    // --- Prototype routes ---\n\n    const itemPageRoute = Movie.prototype.setRoute('ItemPage', '/movies/:id');\n\n    expect(Movie.prototype.getRoute('ItemPage')).toBe(itemPageRoute);\n\n    expect(Movie.prototype.hasRoute('DetailsPage')).toBe(false);\n    expect(() => Movie.prototype.getRoute('DetailsPage')).toThrow(\n      \"The route 'DetailsPage' is missing (component: 'Movie')\"\n    );\n  });\n\n  test('setRoute()', async () => {\n    class Movie extends Routable(Component) {}\n\n    class ExtendedMovie extends Movie {}\n\n    // --- Class routes ---\n\n    expect(Movie.hasRoute('ListPage')).toBe(false);\n\n    const listPageRoute = Movie.setRoute('ListPage', '/movies');\n\n    expect(Movie.getRoute('ListPage')).toBe(listPageRoute);\n\n    // - Testing route inheritance -\n\n    expect(ExtendedMovie.hasRoute('HotPage')).toBe(false);\n\n    const hotPageRoute = ExtendedMovie.setRoute('HotPage', '/movies/hot');\n\n    expect(ExtendedMovie.getRoute('HotPage')).toBe(hotPageRoute);\n    expect(ExtendedMovie.getRoute('ListPage')).toBe(listPageRoute);\n    expect(Movie.hasRoute('HotPage')).toBe(false);\n\n    // --- Prototype routes ---\n\n    expect(Movie.prototype.hasRoute('ItemPage')).toBe(false);\n\n    const itemPageRoute = Movie.prototype.setRoute('ItemPage', '/movies/:id');\n\n    expect(Movie.prototype.getRoute('ItemPage')).toBe(itemPageRoute);\n\n    // - Testing route inheritance -\n\n    expect(ExtendedMovie.prototype.hasRoute('DetailsPage')).toBe(false);\n\n    const detailsPageRoute = ExtendedMovie.prototype.setRoute('DetailsPage', '/movies/:id/details');\n\n    expect(ExtendedMovie.prototype.getRoute('DetailsPage')).toBe(detailsPageRoute);\n    expect(ExtendedMovie.prototype.getRoute('ItemPage')).toBe(itemPageRoute);\n    expect(Movie.prototype.hasRoute('DetailsPage')).toBe(false);\n  });\n\n  test('findRouteByURL()', async () => {\n    class Movie extends Routable(Component) {}\n\n    // --- Class routes ---\n\n    const listPageRoute = Movie.setRoute('ListPage', '/movies');\n    const hotPageRoute = Movie.setRoute('HotPage', '/movies/hot');\n\n    expect(Movie.findRouteByURL('/movies')).toStrictEqual({\n      route: listPageRoute,\n      identifiers: {},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(Movie.findRouteByURL('/movies/hot')).toStrictEqual({\n      route: hotPageRoute,\n      identifiers: {},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(Movie.findRouteByURL('/films')).toBeUndefined();\n\n    // --- Prototype routes ---\n\n    const itemPageRoute = Movie.prototype.setRoute('ItemPage', '/movies/:id');\n    const detailsPageRoute = Movie.prototype.setRoute('DetailsPage', '/movies/:id/details');\n\n    expect(Movie.prototype.findRouteByURL('/movies/abc123')).toStrictEqual({\n      route: itemPageRoute,\n      identifiers: {id: 'abc123'},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(Movie.prototype.findRouteByURL('/movies/abc123/details')).toStrictEqual({\n      route: detailsPageRoute,\n      identifiers: {id: 'abc123'},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(Movie.prototype.findRouteByURL('/films/abc123')).toBeUndefined();\n\n    // --- Catch-all routes ---\n\n    const notFoundPageRoute = Movie.setRoute('NotFoundPage', '/*');\n\n    expect(Movie.findRouteByURL('/movies')).toStrictEqual({\n      route: listPageRoute,\n      identifiers: {},\n      params: {},\n      wrapperPath: undefined\n    });\n\n    expect(Movie.findRouteByURL('/films')).toStrictEqual({\n      route: notFoundPageRoute,\n      identifiers: {},\n      params: {},\n      wrapperPath: undefined\n    });\n\n    // Let's make sure that a route defined after a catch-all route is working...\n    const actorListPageRoute = Movie.setRoute('ActorListPage', '/actors');\n\n    expect(Movie.findRouteByURL('/actors')).toStrictEqual({\n      route: actorListPageRoute,\n      identifiers: {},\n      params: {},\n      wrapperPath: undefined\n    });\n\n    // ... and that the catch-all route is working too\n    expect(Movie.findRouteByURL('/films')).toStrictEqual({\n      route: notFoundPageRoute,\n      identifiers: {},\n      params: {},\n      wrapperPath: undefined\n    });\n  });\n\n  test('callRouteByURL()', async () => {\n    class Studio extends Routable(Component) {\n      @primaryIdentifier() id!: string;\n    }\n\n    class Movie extends Routable(Component) {\n      @primaryIdentifier() id!: string;\n\n      @attribute('Studio') studio!: Studio;\n\n      static ListPage() {\n        return `All movies`;\n      }\n\n      ItemPage({showDetails = false}) {\n        return `Movie #${this.id}${showDetails ? ' (with details)' : ''}`;\n      }\n\n      ItemWithStudioPage() {\n        return `Studio #${this.studio.id} > Movie #${this.id}`;\n      }\n    }\n\n    class Application extends Routable(Component) {\n      @provide() static Studio = Studio;\n      @provide() static Movie = Movie;\n\n      static MainLayout({children}: {children: () => any}) {\n        return `[${children()}]`;\n      }\n\n      static echo({message = ''}: {message?: string}) {\n        if (message === 'GET:') {\n          throw new Error(\"'message' cannot be empty\");\n        }\n\n        return message;\n      }\n\n      static async echoAsync({message = ''}: {message?: string}) {\n        if (message === 'GET:') {\n          throw new Error(\"'message' cannot be empty\");\n        }\n\n        return message;\n      }\n    }\n\n    Application.setWrapper('MainLayout', '/');\n\n    // --- Class routes ---\n\n    Movie.setRoute('ListPage', '[/]movies');\n\n    expect(callRouteByURL(Application, '/movies')).toBe('[All movies]');\n\n    expect(() => callRouteByURL(Application, '/movies/hot')).toThrow(\n      \"Couldn't find a route matching the specified URL (URL: '/movies/hot')\"\n    );\n\n    // --- Prototype routes ---\n\n    Movie.prototype.setRoute('ItemPage', '[/]movies/:id', {params: {showDetails: 'boolean?'}});\n\n    expect(callRouteByURL(Application, '/movies/abc123')).toBe('[Movie #abc123]');\n    expect(callRouteByURL(Application, '/movies/abc123?showDetails=1')).toBe(\n      '[Movie #abc123 (with details)]'\n    );\n\n    expect(() => callRouteByURL(Application, '/movies/abc123/details')).toThrow(\n      \"Couldn't find a route matching the specified URL (URL: '/movies/abc123/details')\"\n    );\n\n    Movie.prototype.setRoute('ItemWithStudioPage', '[/]studios/:studio.id/movies/:id');\n\n    expect(callRouteByURL(Application, '/studios/abc123/movies/def456')).toBe(\n      '[Studio #abc123 > Movie #def456]'\n    );\n\n    // --- Route transformers ---\n\n    const transformers = {\n      input({message = ''}: {message?: string}, {method}: {method: string}) {\n        return {message: `${method}:${message}`};\n      },\n      output(result: string) {\n        return {\n          status: 200,\n          headers: {'content-type': 'application/json'},\n          body: JSON.stringify(result)\n        };\n      },\n      error(error: Error) {\n        return {\n          status: 400,\n          headers: {'content-type': 'application/json'},\n          body: JSON.stringify({message: error.message})\n        };\n      }\n    };\n\n    // - With a synchronous method -\n\n    Application.setRoute('echo', '/echo', {params: {message: 'string?'}, transformers});\n\n    expect(callRouteByURL(Application, '/echo?message=hello', {method: 'GET'})).toStrictEqual({\n      status: 200,\n      headers: {'content-type': 'application/json'},\n      body: '\"GET:hello\"'\n    });\n    expect(callRouteByURL(Application, '/echo', {method: 'GET'})).toStrictEqual({\n      status: 400,\n      headers: {'content-type': 'application/json'},\n      body: `{\"message\":\"'message' cannot be empty\"}`\n    });\n\n    // - With an asynchronous method -\n\n    Application.setRoute('echoAsync', '/echo-async', {params: {message: 'string?'}, transformers});\n\n    expect(\n      await callRouteByURL(Application, '/echo-async?message=hello', {method: 'GET'})\n    ).toStrictEqual({\n      status: 200,\n      headers: {'content-type': 'application/json'},\n      body: '\"GET:hello\"'\n    });\n    expect(await callRouteByURL(Application, '/echo-async', {method: 'GET'})).toStrictEqual({\n      status: 400,\n      headers: {'content-type': 'application/json'},\n      body: `{\"message\":\"'message' cannot be empty\"}`\n    });\n  });\n});\n"
  },
  {
    "path": "packages/routable/src/routable.ts",
    "content": "import {\n  Component,\n  isComponentClass,\n  assertIsComponentClass,\n  isComponentValueTypeInstance\n} from '@layr/component';\nimport {Navigator, assertIsNavigatorInstance, normalizeURL} from '@layr/navigator';\nimport {hasOwnProperty, getTypeOf, Constructor} from 'core-helpers';\nimport debugModule from 'debug';\n\nimport {Route, RouteOptions} from './route';\nimport {Wrapper, WrapperOptions} from './wrapper';\nimport type {Pattern} from './pattern';\nimport {isRoutableClass, isRoutableInstance} from './utilities';\n\nconst debug = debugModule('layr:routable');\n// To display the debug log, set this environment:\n// DEBUG=layr:routable DEBUG_DEPTH=10\n\n/**\n * Extends a [`Component`](https://layrjs.com/docs/v2/reference/component) class with some routing capabilities.\n *\n * #### Usage\n *\n * Call `Routable()` with a [`Component`](https://layrjs.com/docs/v2/reference/component) class to construct a [`RoutableComponent`](https://layrjs.com/docs/v2/reference/routable#routable-component-class) class.\n *\n * Then, you can define some routes or wrappers into this class by using the [`@route()`](https://layrjs.com/docs/v2/reference/routable#route-decorator) or [`@wrapper()`](https://layrjs.com/docs/v2/reference/routable#wrapper-decorator) decorators.\n *\n * See an example of use in the [`BrowserNavigatorView`](https://layrjs.com/docs/v2/reference/react-integration#browser-navigator-view-react-component) React component.\n *\n * ### RoutableComponent <badge type=\"primary\">class</badge> {#routable-component-class}\n *\n * *Inherits from [`Component`](https://layrjs.com/docs/v2/reference/component).*\n *\n * A `RoutableComponent` class is constructed by calling the `Routable()` mixin ([see above](https://layrjs.com/docs/v2/reference/routable#routable-mixin)).\n *\n * @mixin\n */\nexport function Routable<T extends Constructor<typeof Component>>(Base: T) {\n  if (!isComponentClass(Base)) {\n    throw new Error(\n      `The Routable mixin should be applied on a component class (received type: '${getTypeOf(\n        Base\n      )}')`\n    );\n  }\n\n  if (typeof (Base as any).isRoutable === 'function') {\n    return Base as T & typeof Routable;\n  }\n\n  class Routable extends Base {\n    // === Component Methods ===\n\n    /**\n     * See the methods that are inherited from the [`Component`](https://layrjs.com/docs/v2/reference/component#creation) class.\n     *\n     * @category Component Methods\n     */\n\n    // === Navigator ===\n\n    static __navigator: Navigator | undefined;\n\n    static registerNavigator(navigator: Navigator) {\n      assertIsNavigatorInstance(navigator);\n\n      Object.defineProperty(this, '__navigator', {value: navigator});\n    }\n\n    /**\n     * Returns the navigator in which the routable component is registered. If the routable component is not registered in a navigator, an error is thrown.\n     *\n     * @returns A [`Navigator`](https://layrjs.com/docs/v2/reference/navigator) instance.\n     *\n     * @example\n     * ```\n     * Article.getNavigator(); // => navigator instance\n     * ```\n     *\n     * @category Navigator\n     */\n    static getNavigator() {\n      const navigator = this.findNavigator();\n\n      if (navigator === undefined) {\n        throw new Error(\n          `Couldn't find a navigator from the current routable component (${this.describeComponent()})`\n        );\n      }\n\n      return navigator;\n    }\n\n    /**\n     * Returns the navigator in which the routable component is registered. If the routable component is not registered in a navigator, an error is thrown.\n     *\n     * @returns A [`Navigator`](https://layrjs.com/docs/v2/reference/navigator) instance.\n     *\n     * @example\n     * ```\n     * Article.getNavigator(); // => navigator instance\n     * ```\n     *\n     * @category Navigator\n     */\n    getNavigator() {\n      return (this.constructor as typeof Routable).getNavigator();\n    }\n\n    static findNavigator() {\n      let currentComponent: typeof Component = this;\n\n      while (true) {\n        if (isRoutableClass(currentComponent) && currentComponent.__navigator !== undefined) {\n          return currentComponent.__navigator;\n        }\n\n        const componentProvider = currentComponent.getComponentProvider();\n\n        if (componentProvider === currentComponent) {\n          break;\n        }\n\n        currentComponent = componentProvider;\n      }\n\n      return undefined;\n    }\n\n    findNavigator() {\n      return (this.constructor as typeof Routable).findNavigator();\n    }\n\n    // === Routes ===\n\n    /**\n     * Gets a route. If there is no route with the specified name, an error is thrown.\n     *\n     * @param name The name of the route to get.\n     *\n     * @returns A [Route](https://layrjs.com/docs/v2/reference/route) instance.\n     *\n     * @example\n     * ```\n     * Article.getRoute('View'); // A route instance\n     * ```\n     *\n     * @category Routes\n     */\n    static get getRoute() {\n      return (this.prototype as Routable).getRoute;\n    }\n\n    /**\n     * Gets a route. If there is no route with the specified name, an error is thrown.\n     *\n     * @param name The name of the route to get.\n     *\n     * @returns A [Route](https://layrjs.com/docs/v2/reference/route) instance.\n     *\n     * @example\n     * ```\n     * Article.getRoute('View'); // A route instance\n     * ```\n     *\n     * @category Routes\n     */\n    getRoute(name: string) {\n      const route = this.__getRoute(name);\n\n      if (route === undefined) {\n        throw new Error(`The route '${name}' is missing (${this.describeComponent()})`);\n      }\n\n      return route;\n    }\n\n    /**\n     * Returns whether the routable component has a route with the specified name.\n     *\n     * @param name The name of the route to check.\n     *\n     * @returns A boolean.\n     *\n     * @example\n     * ```\n     * Article.hasRoute('View'); // => true\n     * ```\n     *\n     * @category Routes\n     */\n    static get hasRoute() {\n      return (this.prototype as Routable).hasRoute;\n    }\n\n    /**\n     * Returns whether the routable component has a route with the specified name.\n     *\n     * @param name The name of the route to check.\n     *\n     * @returns A boolean.\n     *\n     * @example\n     * ```\n     * Article.hasRoute('View'); // => true\n     * ```\n     *\n     * @category Routes\n     */\n    hasRoute(name: string) {\n      return this.__getRoute(name) !== undefined;\n    }\n\n    static get __getRoute() {\n      return (this.prototype as Routable).__getRoute;\n    }\n\n    __getRoute(name: string) {\n      const routes = this.__getRoutes();\n\n      return routes.get(name);\n    }\n\n    /**\n     * Sets a route for a routable component class or instances.\n     *\n     * Typically, instead of using this method, you would rather use the [`@route()`](https://layrjs.com/docs/v2/reference/routable#route-decorator) decorator.\n     *\n     * @param name The name of the route.\n     * @param pattern The canonical [URL pattern](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type) of the route.\n     * @param [options] An optional object specifying the options to pass to the `Route`'s [constructor](https://layrjs.com/docs/v2/reference/addressable#constructor) when the route is created.\n     *\n     * @returns The [Route](https://layrjs.com/docs/v2/reference/route) instance that was created.\n     *\n     * @example\n     * ```\n     * Article.setRoute('View', '/articles', {parameters: {page: 'number?'});\n     *\n     * Article.prototype.setRoute('View', '/articles/:id', {parameters: {showDetails: 'boolean?'}});\n     * ```\n     *\n     * @category Routes\n     */\n    static get setRoute() {\n      return (this.prototype as Routable).setRoute;\n    }\n\n    /**\n     * Sets a route for a routable component class or instances.\n     *\n     * Typically, instead of using this method, you would rather use the [`@route()`](https://layrjs.com/docs/v2/reference/routable#route-decorator) decorator.\n     *\n     * @param name The name of the route.\n     * @param pattern The canonical [URL pattern](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type) of the route.\n     * @param [options] An optional object specifying the options to pass to the `Route`'s [constructor](https://layrjs.com/docs/v2/reference/addressable#constructor) when the route is created.\n     *\n     * @returns The [Route](https://layrjs.com/docs/v2/reference/route) instance that was created.\n     *\n     * @example\n     * ```\n     * Article.setRoute('View', '/articles', {parameters: {page: 'number?'});\n     *\n     * Article.prototype.setRoute('View', '/articles/:id', {parameters: {showDetails: 'boolean?'}});\n     * ```\n     *\n     * @category Routes\n     */\n    setRoute(name: string, pattern: Pattern, options: RouteOptions = {}) {\n      const route = new Route(name, pattern, options);\n\n      const routes = this.__getRoutes(true);\n\n      routes.set(name, route);\n\n      return route;\n    }\n\n    static get __callRoute() {\n      return (this.prototype as Routable).__callRoute;\n    }\n\n    __callRoute(\n      rootComponent: typeof Component,\n      route: Route,\n      identifiers: any,\n      params: any,\n      wrapperPath: string | undefined,\n      request: any\n    ) {\n      const name = route.getName();\n\n      debug('Calling route %s(%o)', this.describeComponentProperty(name), params);\n\n      const component = instantiateComponent(this, identifiers);\n\n      const method = route.transformMethod((component as any)[name], request);\n\n      const navigator = this.findNavigator();\n\n      if (wrapperPath !== undefined) {\n        return callWrapperByPath(\n          rootComponent,\n          wrapperPath,\n          function () {\n            if (navigator !== undefined) {\n              return navigator.callAddressableMethodWrapper(component, method, params);\n            } else {\n              return method.call(component, params);\n            }\n          },\n          request\n        );\n      } else {\n        if (navigator !== undefined) {\n          return navigator.callAddressableMethodWrapper(component, method, params);\n        } else {\n          return method.call(component, params);\n        }\n      }\n    }\n\n    /**\n     * Finds the first route that matches the specified URL.\n     *\n     * @param url A string or a [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) object.\n     *\n     * @returns When a route is found, returns an object of the shape `{route, identifiers, params}` where `route` is the [route](https://layrjs.com/docs/v2/reference/route) that was found, `identifiers` is a plain object containing the value of some [component identifier attributes](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type), and `params` is a plain object containing the value of some [URL parameters](https://layrjs.com/docs/v2/reference/addressable#url-parameters-type). If no routes were found, returns `undefined`.\n     *\n     * @example\n     * ```\n     * const result = Article.prototype.findRouteByURL('/articles/abc123?showDetails=1');\n     *\n     * result.route; // => A route instance\n     * result.identifiers; // => {id: 'abc123'}\n     * result.params; // => {showDetails: true}\n     * ```\n     *\n     * @category Routes\n     */\n    static get findRouteByURL() {\n      return (this.prototype as Routable).findRouteByURL;\n    }\n\n    /**\n     * Finds the first route that matches the specified URL.\n     *\n     * @param url A string or a [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) object.\n     *\n     * @returns When a route is found, returns an object of the shape `{route, identifiers, params}` where `route` is the [route](https://layrjs.com/docs/v2/reference/route) that was found, `identifiers` is a plain object containing the value of some [component identifier attributes](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type), and `params` is a plain object containing the value of some [URL parameters](https://layrjs.com/docs/v2/reference/addressable#url-parameters-type). If no routes were found, returns `undefined`.\n     *\n     * @example\n     * ```\n     * const result = Article.prototype.findRouteByURL('/articles/abc123?showDetails=1');\n     *\n     * result.route; // => A route instance\n     * result.identifiers; // => {id: 'abc123'}\n     * result.params; // => {showDetails: true}\n     * ```\n     *\n     * @category Routes\n     */\n    findRouteByURL(url: URL | string, request?: any) {\n      const normalizedURL = normalizeURL(url);\n\n      const routes = this.__getRoutes();\n\n      let result:\n        | ({\n            route: Route;\n          } & ReturnType<Route['matchURL']>)\n        | undefined;\n\n      for (const route of routes.values()) {\n        const routeResult = route.matchURL(normalizedURL, request);\n\n        if (routeResult !== undefined) {\n          result = {route, ...routeResult};\n\n          if (!result.route.isCatchAll()) {\n            break;\n          }\n        }\n      }\n\n      return result;\n    }\n\n    static __routes?: Map<string, Route>;\n\n    __routes?: Map<string, Route>;\n\n    static get __getRoutes() {\n      return (this.prototype as Routable).__getRoutes;\n    }\n\n    __getRoutes(autoFork = false) {\n      if (this.__routes === undefined) {\n        Object.defineProperty(this, '__routes', {value: new Map()});\n      } else if (autoFork && !hasOwnProperty(this, '__routes')) {\n        Object.defineProperty(this, '__routes', {value: new Map(this.__routes)});\n      }\n\n      return this.__routes!;\n    }\n\n    // === Wrappers ===\n\n    /**\n     * Gets a wrapper. If there is no wrapper with the specified name, an error is thrown.\n     *\n     * @param name The name of the wrapper to get.\n     *\n     * @returns A [Wrapper](https://layrjs.com/docs/v2/reference/wrapper) instance.\n     *\n     * @example\n     * ```\n     * Article.getWrapper('Layout'); => A wrapper instance\n     * ```\n     *\n     * @category Wrappers\n     */\n    static get getWrapper() {\n      return (this.prototype as Routable).getWrapper;\n    }\n\n    /**\n     * Gets a wrapper. If there is no wrapper with the specified name, an error is thrown.\n     *\n     * @param name The name of the wrapper to get.\n     *\n     * @returns A [Wrapper](https://layrjs.com/docs/v2/reference/wrapper) instance.\n     *\n     * @example\n     * ```\n     * Article.getWrapper('Layout'); => A wrapper instance\n     * ```\n     *\n     * @category Wrappers\n     */\n    getWrapper(name: string) {\n      const wrapper = this.__getWrapper(name);\n\n      if (wrapper === undefined) {\n        throw new Error(`The wrapper '${name}' is missing (${this.describeComponent()})`);\n      }\n\n      return wrapper;\n    }\n\n    /**\n     * Returns whether the routable component has a wrapper with the specified name.\n     *\n     * @param name The name of the wrapper to check.\n     *\n     * @returns A boolean.\n     *\n     * @example\n     * ```\n     * Article.hasWrapper('Layout'); // => true\n     * ```\n     *\n     * @category Wrappers\n     */\n    static get hasWrapper() {\n      return (this.prototype as Routable).hasWrapper;\n    }\n\n    /**\n     * Returns whether the routable component has a wrapper with the specified name.\n     *\n     * @param name The name of the wrapper to check.\n     *\n     * @returns A boolean.\n     *\n     * @example\n     * ```\n     * Article.hasWrapper('Layout'); // => true\n     * ```\n     *\n     * @category Wrappers\n     */\n    hasWrapper(name: string) {\n      return this.__getWrapper(name) !== undefined;\n    }\n\n    static get __getWrapper() {\n      return (this.prototype as Routable).__getWrapper;\n    }\n\n    __getWrapper(name: string) {\n      const wrappers = this.__getWrappers();\n\n      return wrappers.get(name);\n    }\n\n    /**\n     * Sets a wrapper for a routable component class or instances.\n     *\n     * Typically, instead of using this method, you would rather use the [`@wrapper()`](https://layrjs.com/docs/v2/reference/routable#wrapper-decorator) decorator.\n     *\n     * @param name The name of the wrapper.\n     * @param pattern The canonical [URL pattern](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type) of the wrapper.\n     * @param [options] An optional object specifying the options to pass to the `Wrapper`'s [constructor](https://layrjs.com/docs/v2/reference/addressable#constructor) when the wrapper is created.\n     *\n     * @returns The [Wrapper](https://layrjs.com/docs/v2/reference/wrapper) instance that was created.\n     *\n     * @example\n     * ```\n     * Article.setWrapper('Layout', '/articles');\n     *\n     * Article.prototype.setWrapper('View', '[/articles]/:id');\n     * ```\n     *\n     * @category Wrappers\n     */\n    static get setWrapper() {\n      return (this.prototype as Routable).setWrapper;\n    }\n\n    /**\n     * Sets a wrapper for a routable component class or instances.\n     *\n     * Typically, instead of using this method, you would rather use the [`@wrapper()`](https://layrjs.com/docs/v2/reference/routable#wrapper-decorator) decorator.\n     *\n     * @param name The name of the wrapper.\n     * @param pattern The canonical [URL pattern](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type) of the wrapper.\n     * @param [options] An optional object specifying the options to pass to the `Wrapper`'s [constructor](https://layrjs.com/docs/v2/reference/addressable#constructor) when the wrapper is created.\n     *\n     * @returns The [Wrapper](https://layrjs.com/docs/v2/reference/wrapper) instance that was created.\n     *\n     * @example\n     * ```\n     * Article.setWrapper('Layout', '/articles');\n     *\n     * Article.prototype.setWrapper('View', '[/articles]/:id');\n     * ```\n     *\n     * @category Wrappers\n     */\n    setWrapper(name: string, pattern: Pattern, options: WrapperOptions = {}) {\n      const wrapper = new Wrapper(name, pattern, options);\n\n      const wrappers = this.__getWrappers(true);\n\n      wrappers.set(name, wrapper);\n\n      return wrapper;\n    }\n\n    static get __callWrapper() {\n      return (this.prototype as Routable).__callWrapper;\n    }\n\n    __callWrapper(\n      rootComponent: typeof Component,\n      wrapper: Wrapper,\n      identifiers: any,\n      wrapperPath: string | undefined,\n      children: () => any,\n      request: any\n    ): any {\n      const name = wrapper.getName();\n\n      debug('Calling wrapper %s()', this.describeComponentProperty(name));\n\n      const component = instantiateComponent(this, identifiers);\n\n      const method = wrapper.transformMethod((component as any)[name], request);\n\n      const navigator = this.findNavigator();\n\n      if (wrapperPath !== undefined) {\n        return callWrapperByPath(\n          rootComponent,\n          wrapperPath,\n          function () {\n            if (navigator !== undefined) {\n              return navigator.callAddressableMethodWrapper(component, method, {children});\n            } else {\n              return method.call(component, {children});\n            }\n          },\n          request\n        );\n      } else {\n        if (navigator !== undefined) {\n          return navigator.callAddressableMethodWrapper(component, method, {children});\n        } else {\n          return method.call(component, {children});\n        }\n      }\n    }\n\n    /**\n     * Finds the first wrapper that matches the specified path.\n     *\n     * @param path A string representing a path.\n     *\n     * @returns When a wrapper is found, returns an object of the shape `{wrapper, identifiers}` where `wrapper` is the [wrapper](https://layrjs.com/docs/v2/reference/wrapper) that was found and `identifiers` is a plain object containing the value of some [component identifier attributes](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type). If no wrappers were found, returns `undefined`.\n     *\n     * @example\n     * ```\n     * const result = Article.prototype.findWrapperByPath('/articles/abc123');\n     *\n     * result.wrapper; // => A wrapper instance\n     * result.identifiers; // => {id: 'abc123'}\n     * ```\n     *\n     * @category Wrappers\n     */\n    static get findWrapperByPath() {\n      return (this.prototype as Routable).findWrapperByPath;\n    }\n\n    /**\n     * Finds the first wrapper that matches the specified path.\n     *\n     * @param path A string representing a path.\n     *\n     * @returns When a wrapper is found, returns an object of the shape `{wrapper, identifiers}` where `wrapper` is the [wrapper](https://layrjs.com/docs/v2/reference/wrapper) that was found and `identifiers` is a plain object containing the value of some [component identifier attributes](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type). If no wrappers were found, returns `undefined`.\n     *\n     * @example\n     * ```\n     * const result = Article.prototype.findWrapperByPath('/articles/abc123');\n     *\n     * result.wrapper; // => A wrapper instance\n     * result.identifiers; // => {id: 'abc123'}\n     * ```\n     *\n     * @category Wrappers\n     */\n    findWrapperByPath(path: string, request?: any) {\n      const wrappers = this.__getWrappers();\n\n      for (const wrapper of wrappers.values()) {\n        const result = wrapper.matchPath(path, request);\n\n        if (result !== undefined) {\n          return {wrapper, ...result};\n        }\n      }\n\n      return undefined;\n    }\n\n    static __wrappers?: Map<string, Wrapper>;\n\n    __wrappers?: Map<string, Wrapper>;\n\n    static get __getWrappers() {\n      return (this.prototype as Routable).__getWrappers;\n    }\n\n    __getWrappers(autoFork = false) {\n      if (this.__wrappers === undefined) {\n        Object.defineProperty(this, '__wrappers', {value: new Map()});\n      } else if (autoFork && !hasOwnProperty(this, '__wrappers')) {\n        Object.defineProperty(this, '__wrappers', {value: new Map(this.__wrappers)});\n      }\n\n      return this.__wrappers!;\n    }\n\n    // === Observability ===\n\n    /**\n     * See the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) class.\n     *\n     * @category Observability\n     */\n\n    // === Utilities ===\n\n    static isRoutable(value: any): value is RoutableComponent {\n      return isRoutableInstance(value);\n    }\n  }\n\n  return Routable;\n}\n\nexport class RoutableComponent extends Routable(Component) {}\n\n// === Multi-components functions ===\n\nexport function findRouteByURL(rootComponent: typeof Component, url: URL | string, request?: any) {\n  assertIsComponentClass(rootComponent);\n\n  const normalizedURL = normalizeURL(url);\n\n  let result:\n    | ({\n        routable: typeof RoutableComponent | RoutableComponent;\n      } & ReturnType<RoutableComponent['findRouteByURL']>)\n    | undefined;\n\n  for (const component of rootComponent.traverseComponents()) {\n    if (!isRoutableClass(component)) {\n      continue;\n    }\n\n    let routable: typeof RoutableComponent | RoutableComponent = component;\n\n    let routableResult = routable.findRouteByURL(normalizedURL, request);\n\n    if (routableResult !== undefined) {\n      result = {routable, ...routableResult};\n\n      if (!result!.route.isCatchAll()) {\n        break;\n      }\n    }\n\n    routable = component.prototype;\n\n    routableResult = routable.findRouteByURL(normalizedURL, request);\n\n    if (routableResult !== undefined) {\n      result = {routable, ...routableResult};\n\n      if (!result!.route.isCatchAll()) {\n        break;\n      }\n    }\n  }\n\n  return result;\n}\n\nexport function callRouteByURL(rootComponent: typeof Component, url: URL | string, request?: any) {\n  const result = findRouteByURL(rootComponent, url, request);\n\n  if (result === undefined) {\n    throw new Error(`Couldn't find a route matching the specified URL (URL: '${url}')`);\n  }\n\n  const {routable, route, identifiers, params, wrapperPath} = result;\n\n  return routable.__callRoute(rootComponent, route, identifiers, params, wrapperPath, request);\n}\n\nexport function findWrapperByPath(rootComponent: typeof Component, path: string, request?: any) {\n  assertIsComponentClass(rootComponent);\n\n  for (const component of rootComponent.traverseComponents()) {\n    if (!isRoutableClass(component)) {\n      continue;\n    }\n\n    let routable: typeof RoutableComponent | RoutableComponent = component;\n\n    let result = routable.findWrapperByPath(path, request);\n\n    if (result !== undefined) {\n      return {routable, ...result};\n    }\n\n    routable = component.prototype;\n\n    result = routable.findWrapperByPath(path, request);\n\n    if (result !== undefined) {\n      return {routable, ...result};\n    }\n  }\n\n  return undefined;\n}\n\nexport function callWrapperByPath(\n  rootComponent: typeof Component,\n  path: string,\n  children: () => any,\n  request?: any\n) {\n  const result = findWrapperByPath(rootComponent, path, request);\n\n  if (result === undefined) {\n    throw new Error(`Couldn't find a wrapper matching the specified path (path: '${path}')`);\n  }\n\n  const {routable, wrapper, identifiers, wrapperPath} = result;\n\n  return routable.__callWrapper(\n    rootComponent,\n    wrapper,\n    identifiers,\n    wrapperPath,\n    children,\n    request\n  );\n}\n\nfunction instantiateComponent(\n  componentClassOrPrototype: typeof Component | Component,\n  identifiers: any\n) {\n  if (isComponentClass(componentClassOrPrototype)) {\n    return componentClassOrPrototype;\n  }\n\n  const componentClass = componentClassOrPrototype.constructor;\n\n  let componentIdentifier: any;\n  let referencedComponentIdentifiers: any = {};\n\n  for (const [name, value] of Object.entries(identifiers)) {\n    if (typeof value === 'string' || typeof value === 'number') {\n      if (componentIdentifier !== undefined) {\n        throw new Error(\n          `Cannot get or instantiate a component with more than one identifier (\\`${JSON.stringify(\n            componentIdentifier\n          )}\\` and \\`${JSON.stringify({[name]: value})}\\`)`\n        );\n      }\n\n      componentIdentifier = {[name]: value};\n    } else if (typeof value === 'object' && value !== null) {\n      referencedComponentIdentifiers[name] = value;\n    } else {\n      throw new Error(\n        `Unexpected identifier type encountered while getting or instantiating a component (type: '${getTypeOf(\n          value\n        )}')`\n      );\n    }\n  }\n\n  if (componentIdentifier === undefined) {\n    throw new Error(`Cannot get or instantiate a component with no specified identifier`);\n  }\n\n  const component = componentClass.instantiate(componentIdentifier, {source: 'server'});\n\n  for (const [name, identifiers] of Object.entries(referencedComponentIdentifiers)) {\n    const attribute = component.getAttribute(name);\n    const valueType = attribute.getValueType();\n\n    if (!isComponentValueTypeInstance(valueType)) {\n      throw new Error(\n        `Unexpected attribute type encountered while getting or instantiating a component (name: '${name}', type: '${valueType.toString()}')`\n      );\n    }\n\n    const referencedComponentClassOrPrototype = valueType.getComponent(attribute);\n\n    attribute.setValue(instantiateComponent(referencedComponentClassOrPrototype, identifiers), {\n      source: 'server'\n    });\n  }\n\n  return component;\n}\n"
  },
  {
    "path": "packages/routable/src/route.test.ts",
    "content": "import {Route, isRouteInstance} from './route';\n\ndescribe('Route', () => {\n  test('new Route()', async () => {\n    let route = new Route('Main', '/movies');\n\n    expect(isRouteInstance(route)).toBe(true);\n\n    expect(route.getName()).toBe('Main');\n    expect(route.getPattern()).toBe('/movies');\n    expect(route.getParams()).toStrictEqual({});\n    expect(route.getAliases()).toStrictEqual([]);\n    expect(route.getFilter()).toBeUndefined();\n    expect(route.getTransformers()).toStrictEqual({});\n\n    // --- Using aliases ---\n\n    route = new Route('Main', '/movies', {aliases: ['/']});\n\n    expect(route.getName()).toBe('Main');\n    expect(route.getPattern()).toBe('/movies');\n    expect(route.getParams()).toStrictEqual({});\n    expect(route.getAliases()).toStrictEqual(['/']);\n    expect(route.getFilter()).toBeUndefined();\n    expect(route.getTransformers()).toStrictEqual({});\n\n    // -- Using route identifiers ---\n\n    route = new Route('Main', '/movies/:id');\n\n    expect(route.getName()).toBe('Main');\n    expect(route.getPattern()).toBe('/movies/:id');\n    expect(route.getParams()).toStrictEqual({});\n    expect(route.getAliases()).toStrictEqual([]);\n    expect(route.getFilter()).toBeUndefined();\n    expect(route.getTransformers()).toStrictEqual({});\n\n    // -- Using route parameters ---\n\n    route = new Route('Main', '/movies', {params: {showDetails: 'boolean?'}});\n\n    expect(route.getName()).toBe('Main');\n    expect(route.getPattern()).toBe('/movies');\n    expect(route.getParams()).toStrictEqual({showDetails: 'boolean?'});\n    expect(route.getAliases()).toStrictEqual([]);\n    expect(route.getFilter()).toBeUndefined();\n    expect(route.getTransformers()).toStrictEqual({});\n\n    // @ts-expect-error\n    expect(() => new Route('Main', '/movies', {params: {showDetails: 'any'}})).toThrow(\n      \"Couldn't parse a route (or wrapper) parameter type ('any' is not a supported type)\"\n    );\n\n    // -- Using route filters ---\n\n    const filter = function (request: any) {\n      return request?.method === 'GET';\n    };\n\n    route = new Route('Main', '/movies', {filter});\n\n    expect(route.getName()).toBe('Main');\n    expect(route.getPattern()).toBe('/movies');\n    expect(route.getParams()).toStrictEqual({});\n    expect(route.getAliases()).toStrictEqual([]);\n    expect(route.getFilter()).toBe(filter);\n    expect(route.getTransformers()).toStrictEqual({});\n\n    // -- Using route transformers ---\n\n    const input = function (params: any) {\n      return params;\n    };\n\n    const output = function (result: any) {\n      return result;\n    };\n\n    const error = function (error: any) {\n      throw error;\n    };\n\n    route = new Route('Main', '/movies', {transformers: {input, output, error}});\n\n    expect(route.getName()).toBe('Main');\n    expect(route.getPattern()).toBe('/movies');\n    expect(route.getParams()).toStrictEqual({});\n    expect(route.getAliases()).toStrictEqual([]);\n    expect(route.getFilter()).toBeUndefined();\n    expect(route.getTransformers()).toStrictEqual({input, output, error});\n  });\n\n  test('matchURL()', async () => {\n    let route = new Route('Main', '/movies');\n\n    expect(route.matchURL('/movies')).toStrictEqual({\n      identifiers: {},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(route.matchURL('/movies/abc123')).toBeUndefined();\n    expect(route.matchURL('/films')).toBeUndefined();\n    expect(route.matchURL('/')).toBeUndefined();\n\n    // --- Using aliases ---\n\n    route = new Route('Main', '/movies', {aliases: ['/', '/films']});\n\n    expect(route.matchURL('/movies')).toStrictEqual({\n      identifiers: {},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(route.matchURL('/')).toStrictEqual({\n      identifiers: {},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(route.matchURL('/films')).toStrictEqual({\n      identifiers: {},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(route.matchURL('/motion-pictures')).toBeUndefined();\n\n    // -- Using route identifiers ---\n\n    route = new Route('Main', '/movies/:id', {aliases: ['/films/:id']});\n\n    expect(route.matchURL('/movies/abc123')).toStrictEqual({\n      identifiers: {id: 'abc123'},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(route.matchURL('/movies/group%2F12345')).toStrictEqual({\n      identifiers: {id: 'group/12345'},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(route.matchURL('/films/abc123')).toStrictEqual({\n      identifiers: {id: 'abc123'},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(route.matchURL('/movies')).toBeUndefined();\n    expect(route.matchURL('/movies/')).toBeUndefined();\n    expect(route.matchURL('/movies/abc123/about')).toBeUndefined();\n\n    // -- Using route nested identifiers ---\n\n    route = new Route('Main', '/projects/:project.slug/implementations/:id');\n    expect(route.matchURL('/projects/realworld/implementations/abc123')).toStrictEqual({\n      identifiers: {id: 'abc123', project: {slug: 'realworld'}},\n      params: {},\n      wrapperPath: undefined\n    });\n\n    // --- Using route identifier prefixes ---\n\n    route = new Route('Main', '/@:username');\n\n    expect(route.matchURL('/@john')).toStrictEqual({\n      identifiers: {username: 'john'},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(route.matchURL('/@')).toBeUndefined();\n    expect(route.matchURL('/john')).toBeUndefined();\n\n    // -- Using wrappers ---\n\n    route = new Route('Main', '[]/special');\n\n    expect(route.matchURL('/special')).toStrictEqual({\n      identifiers: {},\n      params: {},\n      wrapperPath: ''\n    });\n\n    route = new Route('Main', '[/]about');\n\n    expect(route.matchURL('/about')).toStrictEqual({\n      identifiers: {},\n      params: {},\n      wrapperPath: '/'\n    });\n\n    route = new Route('Main', '[/projects/:project.slug]/implementations/:id');\n\n    expect(route.matchURL('/projects/realworld/implementations/abc123')).toStrictEqual({\n      identifiers: {id: 'abc123', project: {slug: 'realworld'}},\n      params: {},\n      wrapperPath: '/projects/realworld'\n    });\n\n    // --- Using optional route parameters ---\n\n    route = new Route('Main', '/movies/:id', {\n      params: {language: 'string?', showDetails: 'boolean?'}\n    });\n\n    expect(route.matchURL('/movies/abc123')).toStrictEqual({\n      identifiers: {id: 'abc123'},\n      params: {language: undefined, showDetails: undefined},\n      wrapperPath: undefined\n    });\n    expect(route.matchURL('/movies/abc123?language=fr')).toStrictEqual({\n      identifiers: {id: 'abc123'},\n      params: {language: 'fr', showDetails: undefined},\n      wrapperPath: undefined\n    });\n    expect(route.matchURL('/movies/abc123?language=fr&showDetails=1')).toStrictEqual({\n      identifiers: {id: 'abc123'},\n      params: {language: 'fr', showDetails: true},\n      wrapperPath: undefined\n    });\n    expect(route.matchURL('/movies/abc123?unknownParam=abc')).toStrictEqual({\n      identifiers: {id: 'abc123'},\n      params: {language: undefined, showDetails: undefined},\n      wrapperPath: undefined\n    });\n    expect(() => route.matchURL('/movies/abc123?showDetails=true')).toThrow(\n      \"Couldn't deserialize a route (or wrapper) parameter (name: 'showDetails', value: 'true', type: 'boolean?'\"\n    );\n\n    // --- Using required route parameters ---\n\n    route = new Route('Main', '/', {params: {language: 'string'}});\n\n    expect(route.matchURL('/?language=fr')).toStrictEqual({\n      identifiers: {},\n      params: {language: 'fr'},\n      wrapperPath: undefined\n    });\n\n    expect(() => route.matchURL('/')).toThrow(\n      \"A required route (or wrapper) parameter is missing (name: 'language', type: 'string')\"\n    );\n\n    // -- Using route filters ---\n\n    route = new Route('Main', '/movies', {\n      filter(request: any) {\n        return request?.method === 'GET';\n      }\n    });\n\n    expect(route.matchURL('/movies', {method: 'GET'})).toStrictEqual({\n      identifiers: {},\n      params: {},\n      wrapperPath: undefined\n    });\n    expect(route.matchURL('/movies', {method: 'POST'})).toBeUndefined();\n    expect(route.matchURL('/movies')).toBeUndefined();\n  });\n\n  test('generateURL()', async () => {\n    let route = new Route('Main', '/movies');\n\n    expect(route.generateURL()).toBe('/movies');\n\n    route = new Route('Main', '/movies/:id');\n\n    expect(route.generateURL({id: 'abc123'})).toBe('/movies/abc123');\n    expect(route.generateURL({id: 'group/12345'})).toBe('/movies/group%2F12345');\n\n    // Make sure it works with non-enumerable properties\n    const movie = {};\n    Object.defineProperty(movie, 'id', {value: 'abc123'});\n\n    expect(route.generateURL(movie)).toBe('/movies/abc123');\n\n    expect(() => route.generateURL()).toThrow(\n      \"Couldn't build a route (or wrapper) path from the pattern '/movies/:id' because the identifier 'id' is missing\"\n    );\n    expect(() => route.generateURL({})).toThrow(\n      \"Couldn't build a route (or wrapper) path from the pattern '/movies/:id' because the identifier 'id' is missing\"\n    );\n    expect(() => route.generateURL({id: ''})).toThrow(\n      \"Couldn't build a route (or wrapper) path from the pattern '/movies/:id' because the identifier 'id' is missing\"\n    );\n    expect(() => route.generateURL({ref: 'abc123'})).toThrow(\n      \"Couldn't build a route (or wrapper) path from the pattern '/movies/:id' because the identifier 'id' is missing\"\n    );\n\n    // -- Using route nested identifiers ---\n\n    route = new Route('Main', '/projects/:project.slug/implementations/:id');\n\n    expect(route.generateURL({id: 'abc123', project: {slug: 'realworld'}})).toBe(\n      '/projects/realworld/implementations/abc123'\n    );\n\n    // --- Using route identifier prefixes ---\n\n    route = new Route('Main', '/@:username');\n\n    expect(route.generateURL({username: 'john'})).toBe('/@john');\n    expect(() => route.generateURL({})).toThrow(\n      \"Couldn't build a route (or wrapper) path from the pattern '/@:username' because the identifier 'username' is missing\"\n    );\n    expect(() => route.generateURL({username: ''})).toThrow(\n      \"Couldn't build a route (or wrapper) path from the pattern '/@:username' because the identifier 'username' is missing\"\n    );\n\n    // --- Using route parameters ---\n\n    route = new Route('Main', '/movies/:id', {\n      params: {language: 'string?', showDetails: 'boolean?'}\n    });\n\n    expect(route.generateURL({id: 'abc123'})).toBe('/movies/abc123');\n    expect(route.generateURL({id: 'abc123'}, {language: 'fr'})).toBe('/movies/abc123?language=fr');\n    expect(route.generateURL({id: 'abc123'}, {language: 'fr', showDetails: true})).toBe(\n      '/movies/abc123?language=fr&showDetails=1'\n    );\n    expect(route.generateURL({id: 'abc123'}, {unknownParam: 'abc'})).toBe('/movies/abc123');\n    expect(() => route.generateURL({id: 'abc123'}, {language: 123})).toThrow(\n      \"Couldn't serialize a route (or wrapper) parameter (name: 'language', value: '123', expected type: 'string?', received type: 'number')\"\n    );\n\n    // --- Using the 'hash' option ---\n\n    route = new Route('Main', '/movies/:id');\n\n    expect(route.generateURL({id: 'abc123'}, {}, {hash: 'main'})).toBe('/movies/abc123#main');\n  });\n\n  test('generatePath()', async () => {\n    const route = new Route('Main', '/movies/:id', {params: {language: 'string?'}});\n\n    expect(route.generatePath({id: 'abc123'})).toBe('/movies/abc123');\n  });\n\n  test('generateQueryString()', async () => {\n    const route = new Route('Main', '/movies/:id', {params: {language: 'string?'}});\n\n    expect(route.generateQueryString({})).toBe('');\n    expect(route.generateQueryString({language: 'fr'})).toBe('language=fr');\n  });\n});\n"
  },
  {
    "path": "packages/routable/src/route.ts",
    "content": "import {Addressable, AddressableOptions} from './addressable';\n\nexport type RouteOptions = AddressableOptions;\n\n/**\n * *Inherits from [`Addressable`](https://layrjs.com/docs/v2/reference/addressable).*\n *\n * Represents a route in a [routable component](https://layrjs.com/docs/v2/reference/routable#routable-component-class).\n *\n * #### Usage\n *\n * Typically, you create a `Route` and associate it to a routable component by using the [`@route()`](https://layrjs.com/docs/v2/reference/routable#route-decorator) decorator.\n *\n * See an example of use in the [`BrowserNavigatorView`](https://layrjs.com/docs/v2/reference/react-integration#browser-navigator-view-react-component) React component.\n */\nexport class Route extends Addressable {\n  // === Creation ===\n\n  /**\n   * See the constructor that is inherited from the [`Addressable`](https://layrjs.com/docs/v2/reference/addressable#constructor) class.\n   *\n   * @category Creation\n   */\n\n  // === Methods ===\n\n  /**\n   * See the methods that are inherited from the [`Addressable`](https://layrjs.com/docs/v2/reference/addressable#basic-methods) class.\n   *\n   * @category Methods\n   */\n\n  // === Types ===\n\n  /**\n   * See the types that are related to the [`Addressable`](https://layrjs.com/docs/v2/reference/addressable#types) class.\n   *\n   * @category Types\n   */\n\n  static isRoute(value: any): value is Route {\n    return isRouteInstance(value);\n  }\n}\n\n/**\n * Returns whether the specified value is a [`Route`](https://layrjs.com/docs/v2/reference/route) class.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isRouteClass(value: any): value is typeof Route {\n  return typeof value?.isRoute === 'function';\n}\n\n/**\n * Returns whether the specified value is a [`Route`](https://layrjs.com/docs/v2/reference/route) instance.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isRouteInstance(value: any): value is Route {\n  return typeof value?.constructor?.isRoute === 'function';\n}\n"
  },
  {
    "path": "packages/routable/src/utilities.ts",
    "content": "import {getTypeOf} from 'core-helpers';\n\nimport type {RoutableComponent} from './routable';\n\n/**\n * Returns whether the specified value is a routable component class.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isRoutableClass(value: any): value is typeof RoutableComponent {\n  return typeof value?.isRoutable === 'function';\n}\n\n/**\n * Returns whether the specified value is a routable component class.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isRoutableInstance(value: any): value is RoutableComponent {\n  return typeof value?.constructor?.isRoutable === 'function';\n}\n\n/**\n * Returns whether the specified value is a routable component class or instance.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isRoutableClassOrInstance(\n  value: any\n): value is typeof RoutableComponent | RoutableComponent {\n  return (\n    typeof value?.isRoutable === 'function' || typeof value?.constructor?.isRoutable === 'function'\n  );\n}\n\n/**\n * Throws an error if the specified value is not a routable component class.\n *\n * @param value A value of any type.\n *\n * @category Utilities\n */\nexport function assertIsRoutableClass(value: any): asserts value is typeof RoutableComponent {\n  if (!isRoutableClass(value)) {\n    throw new Error(\n      `Expected a routable class, but received a value of type '${getTypeOf(value)}'`\n    );\n  }\n}\n\n/**\n * Throws an error if the specified value is not a routable component instance.\n *\n * @param value A value of any type.\n *\n * @category Utilities\n */\nexport function assertIsRoutableInstance(value: any): asserts value is RoutableComponent {\n  if (!isRoutableInstance(value)) {\n    throw new Error(\n      `Expected a routable component instance, but received a value of type '${getTypeOf(value)}'`\n    );\n  }\n}\n\n/**\n * Throws an error if the specified value is not a routable component class or instance.\n *\n * @param value A value of any type.\n *\n * @category Utilities\n */\nexport function assertIsRoutableClassOrInstance(\n  value: any\n): asserts value is typeof RoutableComponent | RoutableComponent {\n  if (!isRoutableClassOrInstance(value)) {\n    throw new Error(\n      `Expected a routable component class or instance, but received a value of type '${getTypeOf(\n        value\n      )}'`\n    );\n  }\n}\n"
  },
  {
    "path": "packages/routable/src/wrapper.ts",
    "content": "import {Addressable, AddressableOptions} from './addressable';\nimport {Pattern} from './pattern';\n\nexport type WrapperOptions = AddressableOptions;\n\n/**\n * *Inherits from [`Addressable`](https://layrjs.com/docs/v2/reference/addressable).*\n *\n * Represents a wrapper in a [routable component](https://layrjs.com/docs/v2/reference/routable#routable-component-class).\n *\n * #### Usage\n *\n * Typically, you create a `Wrapper` and associate it to a routable component by using the [`@wrapper()`](https://layrjs.com/docs/v2/reference/routable#wrapper-decorator) decorator.\n *\n * See an example of use in the [`BrowserNavigatorView`](https://layrjs.com/docs/v2/reference/react-integration#browser-navigator-view-react-component) React component.\n */\nexport class Wrapper extends Addressable {\n  // === Creation ===\n\n  /**\n   * See the constructor that is inherited from the [`Addressable`](https://layrjs.com/docs/v2/reference/addressable#constructor) class.\n   *\n   * @category Creation\n   */\n\n  constructor(name: string, pattern: Pattern, options: WrapperOptions = {}) {\n    super(name, pattern, options);\n\n    if (this.isCatchAll()) {\n      throw new Error(`Couldn't create the wrapper '${name}' (catch-all wrappers are not allowed)`);\n    }\n  }\n\n  // === Methods ===\n\n  /**\n   * See the methods that are inherited from the [`Addressable`](https://layrjs.com/docs/v2/reference/addressable#basic-methods) class.\n   *\n   * @category Methods\n   */\n\n  // === Types ===\n\n  /**\n   * See the types that are related to the [`Addressable`](https://layrjs.com/docs/v2/reference/addressable#types) class.\n   *\n   * @category Types\n   */\n\n  static isWrapper(value: any): value is Wrapper {\n    return isWrapperInstance(value);\n  }\n}\n\n/**\n * Returns whether the specified value is a [`Wrapper`](https://layrjs.com/docs/v2/reference/wrapper) class.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isWrapperClass(value: any): value is typeof Wrapper {\n  return typeof value?.isWrapper === 'function';\n}\n\n/**\n * Returns whether the specified value is a [`Wrapper`](https://layrjs.com/docs/v2/reference/wrapper) instance.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isWrapperInstance(value: any): value is Wrapper {\n  return typeof value?.constructor?.isWrapper === 'function';\n}\n"
  },
  {
    "path": "packages/routable/tsconfig.json",
    "content": "{\n  \"extends\": \"@mvila/tsconfig\",\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/storable/README.md",
    "content": "# @layr/storable\n\nA mixin providing persistence capabilities to Layr components.\n\n## Installation\n\n```\nnpm install @layr/storable\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/storable/package.json",
    "content": "{\n  \"name\": \"@layr/storable\",\n  \"version\": \"2.0.76\",\n  \"description\": \"A mixin providing persistence capabilities to Layr components\",\n  \"keywords\": [\n    \"layr\",\n    \"component\",\n    \"mixin\",\n    \"storage\",\n    \"persistence\",\n    \"database\",\n    \"orm\",\n    \"odm\"\n  ],\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/layrjs/layr/tree/master/packages/storable\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"dist/node-cjs/index.js\",\n  \"module\": \"dist/node-esm/index.js\",\n  \"engines\": {\n    \"node\": \">=16.0.0\"\n  },\n  \"scripts\": {\n    \"build\": \"dev-tools build:ts-library\",\n    \"prepare\": \"npm run build\",\n    \"test\": \"dev-tools test:ts-library\",\n    \"publish:package\": \"dev-tools publish:package\",\n    \"update\": \"dev-tools update:dependencies\"\n  },\n  \"dependencies\": {\n    \"@layr/component\": \"^2.0.51\",\n    \"core-helpers\": \"^1.0.8\",\n    \"lodash\": \"^4.17.21\",\n    \"tslib\": \"^2.4.1\"\n  },\n  \"devDependencies\": {\n    \"@mvila/dev-tools\": \"^1.3.1\",\n    \"@mvila/tsconfig\": \"^1.0.6\",\n    \"@types/jest\": \"^29.2.5\",\n    \"@types/lodash\": \"^4.14.191\"\n  }\n}\n"
  },
  {
    "path": "packages/storable/src/decorators.test.ts",
    "content": "import {Component} from '@layr/component';\n\nimport {Storable} from './storable';\nimport {attribute, method, loader, finder, index} from './decorators';\nimport {isStorableAttributeInstance, isStorableMethodInstance} from './properties';\nimport {isIndexInstance} from './index-class';\n\ndescribe('Decorators', () => {\n  test('@attribute()', async () => {\n    const beforeLoadHook = function () {};\n    const beforeSaveHook = function () {};\n\n    class Movie extends Storable(Component) {\n      @attribute('number', {beforeLoad: beforeLoadHook}) static limit = 100;\n\n      @attribute('string', {beforeSave: beforeSaveHook}) title = '';\n    }\n\n    const limitAttribute = Movie.getStorableAttribute('limit');\n\n    expect(isStorableAttributeInstance(limitAttribute)).toBe(true);\n    expect(limitAttribute.getName()).toBe('limit');\n    expect(limitAttribute.getParent()).toBe(Movie);\n    expect(limitAttribute.getHook('beforeLoad')).toBe(beforeLoadHook);\n    expect(limitAttribute.hasHook('beforeLoad')).toBe(true);\n    expect(limitAttribute.hasHook('beforeSave')).toBe(false);\n\n    const titleAttribute = Movie.prototype.getStorableAttribute('title');\n\n    expect(isStorableAttributeInstance(titleAttribute)).toBe(true);\n    expect(titleAttribute.getName()).toBe('title');\n    expect(titleAttribute.getParent()).toBe(Movie.prototype);\n    expect(titleAttribute.getHook('beforeSave')).toBe(beforeSaveHook);\n    expect(titleAttribute.hasHook('beforeSave')).toBe(true);\n    expect(titleAttribute.hasHook('beforeLoad')).toBe(false);\n  });\n\n  test('@loader()', async () => {\n    const limitLoader = function () {};\n    const titleLoader = function () {};\n\n    class Movie extends Storable(Component) {\n      @loader(limitLoader) @attribute('number?') static limit: number;\n\n      @loader(titleLoader) @attribute('string') title = '';\n    }\n\n    const limitAttribute = Movie.getStorableAttribute('limit');\n\n    expect(isStorableAttributeInstance(limitAttribute)).toBe(true);\n    expect(limitAttribute.getName()).toBe('limit');\n    expect(limitAttribute.getParent()).toBe(Movie);\n    expect(limitAttribute.getLoader()).toBe(limitLoader);\n    expect(limitAttribute.hasLoader()).toBe(true);\n\n    const titleAttribute = Movie.prototype.getStorableAttribute('title');\n\n    expect(isStorableAttributeInstance(titleAttribute)).toBe(true);\n    expect(titleAttribute.getName()).toBe('title');\n    expect(titleAttribute.getParent()).toBe(Movie.prototype);\n    expect(titleAttribute.getLoader()).toBe(titleLoader);\n    expect(titleAttribute.hasLoader()).toBe(true);\n  });\n\n  test('@finder()', async () => {\n    const hasNoAccessFinder = function () {\n      return {};\n    };\n    const hasAccessLevelFinder = function () {\n      return {};\n    };\n\n    class Movie extends Storable(Component) {\n      @finder(hasNoAccessFinder) @attribute('boolean?') hasNoAccess?: boolean;\n      @finder(hasAccessLevelFinder) @method() hasAccessLevel() {}\n    }\n\n    const hasNoAccessAttribute = Movie.prototype.getStorableAttribute('hasNoAccess');\n\n    expect(isStorableAttributeInstance(hasNoAccessAttribute)).toBe(true);\n    expect(hasNoAccessAttribute.getName()).toBe('hasNoAccess');\n    expect(hasNoAccessAttribute.getParent()).toBe(Movie.prototype);\n    expect(hasNoAccessAttribute.getFinder()).toBe(hasNoAccessFinder);\n    expect(hasNoAccessAttribute.hasFinder()).toBe(true);\n\n    const hasAccessLevelMethod = Movie.prototype.getStorableMethod('hasAccessLevel');\n\n    expect(isStorableMethodInstance(hasAccessLevelMethod)).toBe(true);\n    expect(hasAccessLevelMethod.getName()).toBe('hasAccessLevel');\n    expect(hasAccessLevelMethod.getParent()).toBe(Movie.prototype);\n    expect(hasAccessLevelMethod.getFinder()).toBe(hasAccessLevelFinder);\n    expect(hasAccessLevelMethod.hasFinder()).toBe(true);\n  });\n\n  test('@index()', async () => {\n    @index({year: 'desc', title: 'asc'}, {isUnique: true})\n    class Movie extends Storable(Component) {\n      @index({isUnique: true}) @attribute('string') title!: string;\n\n      @index({direction: 'desc'}) @attribute('number') year!: number;\n    }\n\n    const titleIndex = Movie.prototype.getIndex({title: 'asc'});\n\n    expect(isIndexInstance(titleIndex)).toBe(true);\n    expect(titleIndex.getAttributes()).toStrictEqual({title: 'asc'});\n    expect(titleIndex.getParent()).toBe(Movie.prototype);\n    expect(titleIndex.getOptions().isUnique).toBe(true);\n\n    const yearIndex = Movie.prototype.getIndex({year: 'desc'});\n\n    expect(isIndexInstance(yearIndex)).toBe(true);\n    expect(yearIndex.getAttributes()).toStrictEqual({year: 'desc'});\n    expect(yearIndex.getParent()).toBe(Movie.prototype);\n    expect(yearIndex.getOptions().isUnique).not.toBe(true);\n\n    const compoundIndex = Movie.prototype.getIndex({year: 'desc', title: 'asc'});\n\n    expect(isIndexInstance(compoundIndex)).toBe(true);\n    expect(compoundIndex.getAttributes()).toStrictEqual({year: 'desc', title: 'asc'});\n    expect(compoundIndex.getParent()).toBe(Movie.prototype);\n    expect(compoundIndex.getOptions().isUnique).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/storable/src/decorators.ts",
    "content": "import {\n  Attribute,\n  PrimaryIdentifierAttribute,\n  SecondaryIdentifierAttribute,\n  Method,\n  createAttributeDecorator,\n  createMethodDecorator,\n  isComponentClassOrInstance,\n  isComponentInstance\n} from '@layr/component';\n\nimport {StorableComponent, SortDirection} from './storable';\nimport {\n  StorablePropertyFinder,\n  StorableAttribute,\n  StorableAttributeOptions,\n  StorablePrimaryIdentifierAttribute,\n  StorableSecondaryIdentifierAttribute,\n  StorableAttributeLoader,\n  StorableMethod,\n  StorableMethodOptions\n} from './properties';\nimport type {IndexAttributes} from './index-class';\nimport {isStorableClass, isStorableInstance, isStorableClassOrInstance} from './utilities';\n\ntype StorableAttributeDecoratorOptions = Omit<StorableAttributeOptions, 'value' | 'default'>;\n\n/**\n * Decorates an attribute of a storable component so it can be combined with a [`Loader`](https://layrjs.com/docs/v2/reference/storable-attribute#loader-type), a [`Finder`](https://layrjs.com/docs/v2/reference/storable-property#finder-type), or any kind of [`Hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type).\n *\n * @param [valueType] A string specifying the [type of values](https://layrjs.com/docs/v2/reference/value-type#supported-types) that can be stored in the attribute (default: `'any'`).\n * @param [options] The options to create the [`StorableAttribute`](https://layrjs.com/docs/v2/reference/storable-attribute#constructor).\n *\n * @examplelink See an example of use in the [`StorableAttribute`](https://layrjs.com/docs/v2/reference/storable-attribute) class.\n *\n * @category Decorators\n * @decorator\n */\nexport function attribute(\n  valueType?: string,\n  options?: StorableAttributeDecoratorOptions\n): PropertyDecorator;\nexport function attribute(options?: StorableAttributeDecoratorOptions): PropertyDecorator;\nexport function attribute(\n  valueType?: string | StorableAttributeDecoratorOptions,\n  options?: StorableAttributeDecoratorOptions\n) {\n  return createAttributeDecorator(\n    new Map([\n      [isStorableClassOrInstance, StorableAttribute],\n      [isComponentClassOrInstance, Attribute]\n    ]),\n    'attribute',\n    valueType,\n    options\n  );\n}\n\n/**\n * Decorates an attribute of a component as a [storable primary identifier attribute](https://layrjs.com/docs/v2/reference/storable-primary-identifier-attribute).\n *\n * @param [valueType] A string specifying the type of values the attribute can store. It can be either `'string'` or `'number'` (default: `'string'`).\n * @param [options] The options to create the [`StorablePrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/storable-primary-identifier-attribute#constructor).\n *\n * @category Decorators\n * @decorator\n */\nexport function primaryIdentifier(\n  valueType?: string,\n  options?: StorableAttributeDecoratorOptions\n): PropertyDecorator;\nexport function primaryIdentifier(options?: StorableAttributeDecoratorOptions): PropertyDecorator;\nexport function primaryIdentifier(\n  valueType?: string | StorableAttributeDecoratorOptions,\n  options?: StorableAttributeDecoratorOptions\n) {\n  return createAttributeDecorator(\n    new Map([\n      [isStorableInstance, StorablePrimaryIdentifierAttribute],\n      [isComponentInstance, PrimaryIdentifierAttribute]\n    ]),\n    'primaryIdentifier',\n    valueType,\n    options\n  );\n}\n\n/**\n * Decorates an attribute of a component as a [storable secondary identifier attribute](https://layrjs.com/docs/v2/reference/storable-secondary-identifier-attribute).\n *\n * @param [valueType] A string specifying the type of values the attribute can store. It can be either `'string'` or `'number'` (default: `'string'`).\n * @param [options] The options to create the [`StorableSecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/storable-secondary-identifier-attribute#constructor).\n *\n * @category Decorators\n * @decorator\n */\nexport function secondaryIdentifier(\n  valueType?: string,\n  options?: StorableAttributeDecoratorOptions\n): PropertyDecorator;\nexport function secondaryIdentifier(options?: StorableAttributeDecoratorOptions): PropertyDecorator;\nexport function secondaryIdentifier(\n  valueType?: string | StorableAttributeDecoratorOptions,\n  options?: StorableAttributeDecoratorOptions\n) {\n  return createAttributeDecorator(\n    new Map([\n      [isStorableInstance, StorableSecondaryIdentifierAttribute],\n      [isComponentInstance, SecondaryIdentifierAttribute]\n    ]),\n    'secondaryIdentifier',\n    valueType,\n    options\n  );\n}\n\n/**\n * Decorates a method of a storable component so it can be combined with a [`Finder`](https://layrjs.com/docs/v2/reference/storable-property#finder-type).\n *\n * @param [options] The options to create the [`StorableMethod`](https://layrjs.com/docs/v2/reference/storable-method#constructor).\n *\n * @examplelink See an example of use in the [`StorableMethod`](https://layrjs.com/docs/v2/reference/storable-method) class.\n *\n * @category Decorators\n * @decorator\n */\nexport function method(options: StorableMethodOptions = {}) {\n  return createMethodDecorator(\n    new Map([\n      [isStorableClassOrInstance, StorableMethod],\n      [isComponentClassOrInstance, Method]\n    ]),\n    'method',\n    options\n  );\n}\n\n/**\n * Decorates a storable attribute with a [`Loader`](https://layrjs.com/docs/v2/reference/storable-attribute#loader-type).\n *\n * @param loader A function representing the [`Loader`](https://layrjs.com/docs/v2/reference/storable-attribute#loader-type) of the storable attribute.\n *\n * @examplelink See an example of use in the [`StorableAttribute`](https://layrjs.com/docs/v2/reference/storable-attribute) class.\n *\n * @category Decorators\n * @decorator\n */\nexport function loader(loader: StorableAttributeLoader) {\n  return function (target: typeof StorableComponent | StorableComponent, name: string) {\n    if (!isStorableClassOrInstance(target)) {\n      throw new Error(\n        `@loader() must be used as a storable component attribute decorator (property: '${name}')`\n      );\n    }\n\n    if (\n      !target.hasStorableAttribute(name) ||\n      target.getStorableAttribute(name, {autoFork: false}).getParent() !== target\n    ) {\n      throw new Error(\n        `@loader() must be used in combination with @attribute() (property: '${name}')`\n      );\n    }\n\n    target.getStorableAttribute(name).setLoader(loader);\n  };\n}\n\n/**\n * Decorates a storable attribute or method with a [`Finder`](https://layrjs.com/docs/v2/reference/storable-property#finder-type).\n *\n * @param finder A function representing the [`Finder`](https://layrjs.com/docs/v2/reference/storable-property#finder-type) of the storable attribute or method.\n *\n * @examplelink See an example of use in the [`StorableAttribute`](https://layrjs.com/docs/v2/reference/storable-attribute) and [`StorableMethod`](https://layrjs.com/docs/v2/reference/storable-method) classes.\n *\n * @category Decorators\n * @decorator\n */\nexport function finder(finder: StorablePropertyFinder) {\n  return function (target: StorableComponent, name: string) {\n    if (!isStorableInstance(target)) {\n      throw new Error(\n        `@finder() must be used as a storable component property decorator (property: '${name}')`\n      );\n    }\n\n    if (\n      !target.hasStorableProperty(name) ||\n      target.getStorableProperty(name, {autoFork: false}).getParent() !== target\n    ) {\n      throw new Error(\n        `@finder() must be used in combination with @attribute() or @method() (property: '${name}')`\n      );\n    }\n\n    target.getStorableProperty(name).setFinder(finder);\n  };\n}\n\ntype ClassIndexParam = IndexAttributes;\ntype ClassIndexOptions = {isUnique?: boolean};\ntype AttributeIndexParam = {direction?: SortDirection; isUnique?: boolean};\n\n/**\n * Defines an [index](https://layrjs.com/docs/v2/reference/index) for an attribute or a set of attributes.\n *\n * This decorator is commonly placed before a storable component attribute to define a [single attribute index](https://layrjs.com/docs/v2/reference/index#single-attribute-indexes), but it can also be placed before a storable component class to define a [compound attribute index](https://layrjs.com/docs/v2/reference/index#compound-attribute-indexes).\n *\n * @param [optionsOrAttributes] Depends on the type of index you want to define (see below).\n * @param [options] An object specifying some options in the case of compound attribute index (see below).\n *\n * @details\n * ###### Single Attribute Indexes\n *\n * You can define an index for a single attribute by placing the `@index()` decorator before an attribute definition. In this case, you can specify the following parameters:\n *\n * - `options`:\n *   - `direction`: A string representing the sort direction of the index. The possible values are `'asc'` (ascending) or `'desc'` (descending) and the default value is `'asc'`.\n *   - `isUnique`: A boolean specifying whether the index should hold unique values or not (default: `false`). When set to `true`, the underlying database will prevent you to store an attribute with the same value in multiple storable components.\n *\n * ###### Compound Attribute Indexes\n *\n * You can define an index that combines multiple attributes by placing the `@index()` decorator before a storable component class definition. In this case, you can specify the following parameters:\n *\n * - `attributes`: An object specifying the attributes to be indexed. The shape of the object should be `{attributeName: direction, ...}` where `attributeName` is a string representing the name of an attribute and `direction` is a string representing the sort direction (possible values: `'asc'` or `'desc'`).\n * - `options`:\n *   - `isUnique`: A boolean specifying whether the index should hold unique values or not (default: `false`). When set to `true`, the underlying database will prevent you to store an attribute with the same value in multiple storable components.\n *\n * @example\n * ```\n * // JS\n *\n * import {Component} from '@layr/component';\n * import {Storable, attribute, index} from '@layr/storable';\n *\n * // An index that combines the `year` and `title` attributes:\n * ﹫index({year: 'desc', title: 'asc'})\n * export class Movie extends Storable(Component) {\n *   // An index for the `title` attribute with the `isUnique` option:\n *   @index({isUnique: true}) @attribute('string') title;\n *\n *   // An index for the `year` attribute with the `'desc'` sort direction:\n *   @index({direction: 'desc'}) @attribute('number') year;\n * }\n * ```\n *\n * @example\n * ```\n * // TS\n *\n * import {Component} from '@layr/component';\n * import {Storable, attribute, index} from '@layr/storable';\n *\n * // An index that combines the `year` and `title` attributes:\n * ﹫index({year: 'desc', title: 'asc'})\n * export class Movie extends Storable(Component) {\n *   // An index for the `title` attribute with the `isUnique` option:\n *   @index({isUnique: true}) @attribute('string') title!: string;\n *\n *   // An index for the `year` attribute with the `'desc'` sort direction:\n *   @index({direction: 'desc'}) @attribute('number') year!: string;\n * }\n * ```\n *\n * @category Decorators\n * @decorator\n */\nexport function index(\n  param?: AttributeIndexParam\n): (target: StorableComponent, name: string) => void;\nexport function index(\n  param: ClassIndexParam,\n  options?: ClassIndexOptions\n): (target: typeof StorableComponent) => void;\nexport function index(\n  param: ClassIndexParam | AttributeIndexParam = {},\n  options: ClassIndexOptions = {}\n) {\n  return function (target: typeof StorableComponent | StorableComponent, name?: string) {\n    if (name === undefined) {\n      // Class decorator\n\n      if (!isStorableClass(target)) {\n        throw new Error(\n          `@index() must be used as a storable component class decorator or a storable component attribute decorator`\n        );\n      }\n\n      target.prototype.setIndex(param as ClassIndexParam, options);\n\n      return;\n    }\n\n    // Attribute decorator\n\n    if (!isStorableInstance(target)) {\n      throw new Error(\n        `@index() must be used as a storable component class decorator or a storable component attribute decorator (property: '${name}')`\n      );\n    }\n\n    if (\n      !target.hasProperty(name) ||\n      target.getProperty(name, {autoFork: false}).getParent() !== target\n    ) {\n      throw new Error(\n        `@index() must be used in combination with @attribute() (property: '${name}')`\n      );\n    }\n\n    const {direction = 'asc', isUnique} = param as AttributeIndexParam;\n\n    target.setIndex({[name]: direction}, {isUnique});\n  };\n}\n"
  },
  {
    "path": "packages/storable/src/index-class.test.ts",
    "content": "import {Component, EmbeddedComponent, provide} from '@layr/component';\n\nimport {Storable} from './storable';\nimport {primaryIdentifier, secondaryIdentifier, attribute, loader, method} from './decorators';\nimport {Index} from './index-class';\n\ndescribe('Index', () => {\n  test('Creation', async () => {\n    class Person extends Storable(Component) {\n      @primaryIdentifier() id!: string;\n\n      @attribute('string') fullName!: string;\n    }\n\n    class MovieDetails extends Storable(EmbeddedComponent) {\n      @attribute('number') duration!: number;\n\n      @attribute('string') aspectRatio!: string;\n    }\n\n    class Movie extends Storable(Component) {\n      @provide() static Person = Person;\n\n      @provide() static MovieDetails = MovieDetails;\n\n      @primaryIdentifier() id!: string;\n\n      @secondaryIdentifier() slug!: string;\n\n      @attribute('string') title!: string;\n\n      @attribute('number') year!: number;\n\n      @loader(async function (this: Movie) {\n        await this.load({year: true});\n\n        return this.year <= new Date().getFullYear();\n      })\n      @attribute('boolean')\n      isReleased!: boolean;\n\n      @attribute('object') infos!: any;\n\n      @attribute('object[]') history!: any[];\n\n      @attribute('Person') director!: Person;\n\n      @attribute('Person[]') actors!: Person[];\n\n      @attribute('MovieDetails') details!: MovieDetails;\n\n      @method() play() {}\n    }\n\n    let index = new Index({title: 'asc'}, Movie.prototype);\n\n    expect(Index.isIndex(index)).toBe(true);\n    expect(index.getAttributes()).toStrictEqual({title: 'asc'});\n    expect(index.getParent()).toBe(Movie.prototype);\n    expect(index.getOptions().isUnique).not.toBe(true);\n\n    index = new Index({title: 'desc'}, Movie.prototype, {isUnique: true});\n\n    expect(Index.isIndex(index)).toBe(true);\n    expect(index.getAttributes()).toStrictEqual({title: 'desc'});\n    expect(index.getOptions().isUnique).toBe(true);\n\n    index = new Index({year: 'desc', id: 'asc'}, Movie.prototype);\n\n    expect(Index.isIndex(index)).toBe(true);\n    expect(index.getAttributes()).toStrictEqual({year: 'desc', id: 'asc'});\n\n    expect(() => new Index({}, Movie.prototype)).toThrow(\n      \"Cannot create an index for an empty 'attributes' parameter (component: 'Movie')\"\n    );\n\n    index = new Index({duration: 'asc'}, MovieDetails.prototype);\n\n    expect(Index.isIndex(index)).toBe(true);\n    expect(index.getAttributes()).toStrictEqual({duration: 'asc'});\n    expect(index.getParent()).toBe(MovieDetails.prototype);\n    expect(index.getOptions().isUnique).not.toBe(true);\n\n    // @ts-expect-error\n    expect(() => new Index('title', Movie.prototype)).toThrow(\n      \"Expected a plain object, but received a value of type 'string'\"\n    );\n\n    // @ts-expect-error\n    expect(() => new Index({title: 'asc'}, Movie)).toThrow(\n      \"Expected a storable component instance, but received a value of type 'typeof Movie'\"\n    );\n\n    expect(() => new Index({country: 'asc'}, Movie.prototype)).toThrow(\n      \"Cannot create an index for an attribute that doesn't exist (component: 'Movie', attribute: 'country')\"\n    );\n\n    expect(() => new Index({play: 'asc'}, Movie.prototype)).toThrow(\n      \"Cannot create an index for a property that is not an attribute (component: 'Movie', property: 'play')\"\n    );\n\n    expect(() => new Index({id: 'asc'}, Movie.prototype)).toThrow(\n      \"Cannot explicitly create an index for an identifier attribute (component: 'Movie', attribute: 'id'). Note that this type of attribute is automatically indexed.\"\n    );\n\n    expect(() => new Index({slug: 'asc'}, Movie.prototype)).toThrow(\n      \"Cannot explicitly create an index for an identifier attribute (component: 'Movie', attribute: 'slug'). Note that this type of attribute is automatically indexed.\"\n    );\n\n    expect(() => new Index({infos: 'asc'}, Movie.prototype)).toThrow(\n      \"Cannot create an index for an attribute of type 'object' (component: 'Movie', attribute: 'infos')\"\n    );\n\n    expect(() => new Index({history: 'asc'}, Movie.prototype)).toThrow(\n      \"Cannot create an index for an attribute of type 'object' (component: 'Movie', attribute: 'history')\"\n    );\n\n    expect(() => new Index({director: 'asc'}, Movie.prototype)).toThrow(\n      \"Cannot create an index for an attribute of type 'Component' (component: 'Movie', attribute: 'director'). Note that primary identifier attributes of referenced components are automatically indexed.\"\n    );\n\n    expect(() => new Index({actors: 'asc'}, Movie.prototype)).toThrow(\n      \"Cannot create an index for an attribute of type 'Component' (component: 'Movie', attribute: 'actors'). Note that primary identifier attributes of referenced components are automatically indexed.\"\n    );\n\n    expect(() => new Index({details: 'asc'}, Movie.prototype)).toThrow(\n      \"Cannot create an index for an attribute of type 'Component' (component: 'Movie', attribute: 'details'). Note that primary identifier attributes of referenced components are automatically indexed.\"\n    );\n\n    expect(() => new Index({isReleased: 'asc'}, Movie.prototype)).toThrow(\n      \"Cannot create an index for a computed attribute (component: 'Movie', attribute: 'isReleased')\"\n    );\n\n    // @ts-expect-error\n    expect(() => new Index({title: 'ASC'}, Movie.prototype)).toThrow(\n      \"Cannot create an index with an invalid sort direction (component: 'Movie', attribute: 'title', sort direction: 'ASC')\"\n    );\n\n    // @ts-expect-error\n    expect(() => new Index({title: 'asc'}, Movie.prototype, {unknownOption: 123})).toThrow(\n      \"Did not expect the option 'unknownOption' to exist\"\n    );\n  });\n});\n"
  },
  {
    "path": "packages/storable/src/index-class.ts",
    "content": "import {\n  isAttributeInstance,\n  isIdentifierAttributeInstance,\n  isComponentValueTypeInstance,\n  isObjectValueTypeInstance\n} from '@layr/component';\nimport {assertIsPlainObject, assertNoUnknownOptions} from 'core-helpers';\n\nimport type {StorableComponent, SortDirection} from './storable';\nimport {isStorableAttributeInstance} from './properties/storable-attribute';\nimport {assertIsStorableInstance} from './utilities';\n\nexport type IndexAttributes = {[name: string]: SortDirection};\n\nexport type IndexOptions = {isUnique?: boolean};\n\n/**\n * Represents an index for one or several [attributes](https://layrjs.com/docs/v2/reference/attribute) of a [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class).\n *\n * Once an index is defined for an attribute, all queries involving this attribute (through the [`find()`](https://layrjs.com/docs/v2/reference/storable#find-class-method) or the [`count()`](https://layrjs.com/docs/v2/reference/storable#count-class-method) methods) can be greatly optimized by the storable component's [store](https://layrjs.com/docs/v2/reference/store) and its underlying database.\n *\n * #### Usage\n *\n * ##### Single Attribute Indexes\n *\n * Typically, you create an `Index` for a storable component's attribute by using the [`@index()`](https://layrjs.com/docs/v2/reference/storable#index-decorator) decorator. Then, you call the [`migrateStorables()`](https://layrjs.com/docs/v2/reference/store#migrate-storables-instance-method) method on the storable component's store to effectively create the index in the underlying database.\n *\n * For example, here is how you would define a `Movie` class with some indexes:\n *\n * ```js\n * // JS\n *\n * import {Component} from '@layr/component';\n * import {Storable, primaryIdentifier, attribute, index} from '@layr/storable';\n * import {MongoDBStore} from '@layr/mongodb-store';\n *\n * export class Movie extends Storable(Component) {\n *   // Primary and secondary identifier attributes are automatically indexed,\n *   // so there is no need to define an index for these types of attributes\n *   @primaryIdentifier() id;\n *\n *   // Let's define an index for the `title` attribute\n *   @index() @attribute('string') title;\n * }\n *\n * const store = new MongoDBStore('mongodb://user:pass@host:port/db');\n *\n * store.registerStorable(Movie);\n * ```\n *\n * ```ts\n * // TS\n *\n * import {Component} from '@layr/component';\n * import {Storable, primaryIdentifier, attribute, index} from '@layr/storable';\n * import {MongoDBStore} from '@layr/mongodb-store';\n *\n * export class Movie extends Storable(Component) {\n *   // Primary and secondary identifier attributes are automatically indexed,\n *   // so there is no need to define an index for these types of attributes\n *   @primaryIdentifier() id!: string;\n *\n *   // Let's define an index for the `title` attribute\n *   @index() @attribute('string') title!: string;\n * }\n *\n * const store = new MongoDBStore('mongodb://user:pass@host:port/db');\n *\n * store.registerStorable(Movie);\n * ```\n *\n * Then you can call the [`migrateStorables()`](https://layrjs.com/docs/v2/reference/store#migrate-storables-instance-method) method on the store to create the indexes in the MongoDB database:\n *\n * ```\n * await store.migrateStorables();\n * ```\n *\n * And now that the `title` attribute is indexed, you can make any query on this attribute in a very performant way:\n *\n * ```\n * const movies = await Movie.find({title: 'Inception'});\n * ```\n *\n * ##### Compound Attribute Indexes\n *\n * You can create a compound attribute index to optimize some queries that involve a combination of attributes. To do so, you use the [`@index()`](https://layrjs.com/docs/v2/reference/storable#index-decorator) decorator on the storable component itself:\n *\n * ```js\n * // JS\n *\n * import {Component} from '@layr/component';\n * import {Storable, primaryIdentifier, attribute, index} from '@layr/storable';\n * import {MongoDBStore} from '@layr/mongodb-store';\n *\n * // Let's define a compound attribute index for the combination of the `year`\n * // attribute (descending order) and the `title` attribute (ascending order)\n * ﹫index({year: 'desc', title: 'asc'})\n * export class Movie extends Storable(Component) {\n *   @primaryIdentifier() id;\n *\n *   @attribute('string') title;\n *\n *   @attribute('number') year;\n * }\n *\n * const store = new MongoDBStore('mongodb://user:pass@host:port/db');\n *\n * store.registerStorable(Movie);\n * ```\n *\n * ```ts\n * // TS\n *\n * import {Component} from '@layr/component';\n * import {Storable, primaryIdentifier, attribute, index} from '@layr/storable';\n * import {MongoDBStore} from '@layr/mongodb-store';\n *\n * // Let's define a compound attribute index for the combination of the `year`\n * // attribute (descending order) and the `title` attribute (ascending order)\n * ﹫index({year: 'desc', title: 'asc'})\n * export class Movie extends Storable(Component) {\n *   @primaryIdentifier() id!: string;\n *\n *   @attribute('string') title!: string;\n *\n *   @attribute('number') year!: number;\n * }\n *\n * const store = new MongoDBStore('mongodb://user:pass@host:port/db');\n *\n * store.registerStorable(Movie);\n * ```\n *\n * Then you can call the [`migrateStorables()`](https://layrjs.com/docs/v2/reference/store#migrate-storables-instance-method) method on the store to create the compound attribute index in the MongoDB database:\n *\n * ```\n * await store.migrateStorables();\n * ```\n *\n * And now you can make any query involving a combination of `year` and `title` in a very performant way:\n *\n * ```\n * const movies = await Movie.find(\n *   {year: {$greaterThan: 2010}},\n *   true,\n *   {sort: {year: 'desc', title: 'asc'}}\n * );\n * ```\n */\nexport class Index {\n  _attributes: IndexAttributes;\n  _parent: StorableComponent;\n  _options!: IndexOptions;\n\n  /**\n   * Creates an instance of [`Index`](https://layrjs.com/docs/v2/reference/index).\n   *\n   * @param attributes An object specifying the attributes to be indexed. The shape of the object should be `{attributeName: direction, ...}` where `attributeName` is a string representing the name of an attribute and `direction` is a string representing the sort direction (possible values: `'asc'` or `'desc'`).\n   * @param parent The storable component prototype that owns the index.\n   * @param [options.isUnique] A boolean specifying whether the index should hold unique values or not (default: `false`). When set to `true`, the underlying database will prevent you to store an attribute with the same value in multiple storable components.\n   *\n   * @returns The [`Index`](https://layrjs.com/docs/v2/reference/index) instance that was created.\n   *\n   * @category Creation\n   */\n  constructor(attributes: IndexAttributes, parent: StorableComponent, options: IndexOptions = {}) {\n    assertIsPlainObject(attributes);\n    assertIsStorableInstance(parent);\n\n    for (const [name, direction] of Object.entries(attributes)) {\n      if (!parent.hasProperty(name)) {\n        throw new Error(\n          `Cannot create an index for an attribute that doesn't exist (${parent.describeComponent()}, attribute: '${name}')`\n        );\n      }\n\n      const property = parent.getProperty(name, {autoFork: false});\n\n      if (!isAttributeInstance(property)) {\n        throw new Error(\n          `Cannot create an index for a property that is not an attribute (${parent.describeComponent()}, property: '${name}')`\n        );\n      }\n\n      if (isStorableAttributeInstance(property) && property.isComputed()) {\n        throw new Error(\n          `Cannot create an index for a computed attribute (${parent.describeComponent()}, attribute: '${name}')`\n        );\n      }\n\n      const scalarType = property.getValueType().getScalarType();\n\n      if (isObjectValueTypeInstance(scalarType)) {\n        throw new Error(\n          `Cannot create an index for an attribute of type 'object' (${parent.describeComponent()}, attribute: '${name}')`\n        );\n      }\n\n      if (!(direction === 'asc' || direction === 'desc')) {\n        throw new Error(\n          `Cannot create an index with an invalid sort direction (${parent.describeComponent()}, attribute: '${name}', sort direction: '${direction}')`\n        );\n      }\n    }\n\n    if (Object.keys(attributes).length === 0) {\n      throw new Error(\n        `Cannot create an index for an empty 'attributes' parameter (${parent.describeComponent()})`\n      );\n    }\n\n    if (Object.keys(attributes).length === 1) {\n      const name = Object.keys(attributes)[0];\n      const attribute = parent.getAttribute(name, {autoFork: false});\n\n      if (isIdentifierAttributeInstance(attribute)) {\n        throw new Error(\n          `Cannot explicitly create an index for an identifier attribute (${parent.describeComponent()}, attribute: '${name}'). Note that this type of attribute is automatically indexed.`\n        );\n      }\n\n      const scalarType = attribute.getValueType().getScalarType();\n\n      if (isComponentValueTypeInstance(scalarType)) {\n        throw new Error(\n          `Cannot create an index for an attribute of type 'Component' (${parent.describeComponent()}, attribute: '${name}'). Note that primary identifier attributes of referenced components are automatically indexed.`\n        );\n      }\n    }\n\n    this._attributes = attributes;\n    this._parent = parent;\n\n    this.setOptions(options);\n  }\n\n  /**\n   * Returns the indexed attributes.\n   *\n   * @returns An object of the shape `{attributeName: direction, ...}`.\n   *\n   * @category Basic Methods\n   */\n  getAttributes() {\n    return this._attributes;\n  }\n\n  /**\n   * Returns the parent of the index.\n   *\n   * @returns A storable component prototype.\n   *\n   * @category Basic Methods\n   */\n  getParent() {\n    return this._parent;\n  }\n\n  // === Options ===\n\n  getOptions() {\n    return this._options;\n  }\n\n  setOptions(options: IndexOptions = {}) {\n    const {isUnique, ...unknownOptions} = options;\n\n    assertNoUnknownOptions(unknownOptions);\n\n    this._options = {isUnique};\n  }\n\n  // === Forking ===\n\n  fork(parent: StorableComponent) {\n    const indexFork = Object.create(this) as Index;\n\n    indexFork._parent = parent;\n\n    return indexFork;\n  }\n\n  // === Utilities ===\n\n  static isIndex(value: any): value is Index {\n    return isIndexInstance(value);\n  }\n\n  static _buildIndexKey(attributes: IndexAttributes) {\n    return JSON.stringify(attributes);\n  }\n}\n\n/**\n * Returns whether the specified value is an `Index` class.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isIndexClass(value: any): value is typeof Index {\n  return typeof value?.isIndex === 'function';\n}\n\n/**\n * Returns whether the specified value is an `Index` instance.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isIndexInstance(value: any): value is Index {\n  return isIndexClass(value?.constructor) === true;\n}\n"
  },
  {
    "path": "packages/storable/src/index.ts",
    "content": "export * from './decorators';\nexport * from './index-class';\nexport * from './properties';\nexport * from './operator';\nexport * from './query';\nexport * from './storable';\nexport * from './store-like';\nexport * from './utilities';\n"
  },
  {
    "path": "packages/storable/src/js-tests/decorators.test.js",
    "content": "import {Component} from '@layr/component';\n\nimport {Storable} from '../storable';\nimport {attribute, method, loader, finder, index} from '../decorators';\nimport {isStorableAttributeInstance, isStorableMethodInstance} from '../properties';\nimport {isIndexInstance} from '../index-class';\n\ndescribe('Decorators', () => {\n  test('@attribute()', async () => {\n    const beforeLoadHook = function () {};\n    const beforeSaveHook = function () {};\n\n    class Movie extends Storable(Component) {\n      @attribute('number', {beforeLoad: beforeLoadHook}) static limit = 100;\n\n      @attribute('string', {beforeSave: beforeSaveHook}) title = '';\n    }\n\n    const limitAttribute = Movie.getStorableAttribute('limit');\n\n    expect(isStorableAttributeInstance(limitAttribute)).toBe(true);\n    expect(limitAttribute.getName()).toBe('limit');\n    expect(limitAttribute.getParent()).toBe(Movie);\n    expect(limitAttribute.getHook('beforeLoad')).toBe(beforeLoadHook);\n    expect(limitAttribute.hasHook('beforeLoad')).toBe(true);\n    expect(limitAttribute.hasHook('beforeSave')).toBe(false);\n\n    const titleAttribute = Movie.prototype.getStorableAttribute('title');\n\n    expect(isStorableAttributeInstance(titleAttribute)).toBe(true);\n    expect(titleAttribute.getName()).toBe('title');\n    expect(titleAttribute.getParent()).toBe(Movie.prototype);\n    expect(titleAttribute.getHook('beforeSave')).toBe(beforeSaveHook);\n    expect(titleAttribute.hasHook('beforeSave')).toBe(true);\n    expect(titleAttribute.hasHook('beforeLoad')).toBe(false);\n  });\n\n  test('@loader()', async () => {\n    const limitLoader = function () {};\n    const titleLoader = function () {};\n\n    class Movie extends Storable(Component) {\n      @loader(limitLoader) @attribute('number?') static limit;\n\n      @loader(titleLoader) @attribute('string') title = '';\n    }\n\n    const limitAttribute = Movie.getStorableAttribute('limit');\n\n    expect(isStorableAttributeInstance(limitAttribute)).toBe(true);\n    expect(limitAttribute.getName()).toBe('limit');\n    expect(limitAttribute.getParent()).toBe(Movie);\n    expect(limitAttribute.getLoader()).toBe(limitLoader);\n    expect(limitAttribute.hasLoader()).toBe(true);\n\n    const titleAttribute = Movie.prototype.getStorableAttribute('title');\n\n    expect(isStorableAttributeInstance(titleAttribute)).toBe(true);\n    expect(titleAttribute.getName()).toBe('title');\n    expect(titleAttribute.getParent()).toBe(Movie.prototype);\n    expect(titleAttribute.getLoader()).toBe(titleLoader);\n    expect(titleAttribute.hasLoader()).toBe(true);\n  });\n\n  test('@finder()', async () => {\n    const hasNoAccessFinder = function () {\n      return {};\n    };\n    const hasAccessLevelFinder = function () {\n      return {};\n    };\n\n    class Movie extends Storable(Component) {\n      @finder(hasNoAccessFinder) @attribute('boolean?') hasNoAccess;\n      @finder(hasAccessLevelFinder) @method() hasAccessLevel() {}\n    }\n\n    const hasNoAccessAttribute = Movie.prototype.getStorableAttribute('hasNoAccess');\n\n    expect(isStorableAttributeInstance(hasNoAccessAttribute)).toBe(true);\n    expect(hasNoAccessAttribute.getName()).toBe('hasNoAccess');\n    expect(hasNoAccessAttribute.getParent()).toBe(Movie.prototype);\n    expect(hasNoAccessAttribute.getFinder()).toBe(hasNoAccessFinder);\n    expect(hasNoAccessAttribute.hasFinder()).toBe(true);\n\n    const hasAccessLevelMethod = Movie.prototype.getStorableMethod('hasAccessLevel');\n\n    expect(isStorableMethodInstance(hasAccessLevelMethod)).toBe(true);\n    expect(hasAccessLevelMethod.getName()).toBe('hasAccessLevel');\n    expect(hasAccessLevelMethod.getParent()).toBe(Movie.prototype);\n    expect(hasAccessLevelMethod.getFinder()).toBe(hasAccessLevelFinder);\n    expect(hasAccessLevelMethod.hasFinder()).toBe(true);\n  });\n\n  test('@index()', async () => {\n    @index({year: 'desc', title: 'asc'}, {isUnique: true})\n    class Movie extends Storable(Component) {\n      @index({isUnique: true}) @attribute('string') title;\n\n      @index({direction: 'desc'}) @attribute('number') year;\n    }\n\n    const titleIndex = Movie.prototype.getIndex({title: 'asc'});\n\n    expect(isIndexInstance(titleIndex)).toBe(true);\n    expect(titleIndex.getAttributes()).toStrictEqual({title: 'asc'});\n    expect(titleIndex.getParent()).toBe(Movie.prototype);\n    expect(titleIndex.getOptions().isUnique).toBe(true);\n\n    const yearIndex = Movie.prototype.getIndex({year: 'desc'});\n\n    expect(isIndexInstance(yearIndex)).toBe(true);\n    expect(yearIndex.getAttributes()).toStrictEqual({year: 'desc'});\n    expect(yearIndex.getParent()).toBe(Movie.prototype);\n    expect(yearIndex.getOptions().isUnique).not.toBe(true);\n\n    const compoundIndex = Movie.prototype.getIndex({year: 'desc', title: 'asc'});\n\n    expect(isIndexInstance(compoundIndex)).toBe(true);\n    expect(compoundIndex.getAttributes()).toStrictEqual({year: 'desc', title: 'asc'});\n    expect(compoundIndex.getParent()).toBe(Movie.prototype);\n    expect(compoundIndex.getOptions().isUnique).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/storable/src/js-tests/jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"module\": \"CommonJS\",\n    \"checkJs\": true,\n    \"experimentalDecorators\": true\n  }\n}\n"
  },
  {
    "path": "packages/storable/src/operator.ts",
    "content": "import {getTypeOf} from 'core-helpers';\n\nimport type {Query} from './query';\n\nexport type Operator = string;\n\nconst basicOperators = new Set<Operator>([\n  '$equal',\n  '$notEqual',\n  '$greaterThan',\n  '$greaterThanOrEqual',\n  '$lessThan',\n  '$lessThanOrEqual',\n  '$in'\n]);\n\nconst stringOperators = new Set<Operator>(['$includes', '$startsWith', '$endsWith', '$matches']);\n\nconst arrayOperators = new Set<Operator>(['$some', '$every', '$length']);\n\nconst logicalOperators = new Set<Operator>(['$not', '$and', '$or', '$nor']);\n\nconst aliases = new Map<Operator, Operator>(Object.entries({}));\n\nexport function looksLikeOperator(string: string): string is Operator {\n  return string.startsWith('$');\n}\n\nexport function normalizeOperatorForValue(\n  operator: Operator,\n  value: unknown,\n  {query}: {query: Query}\n): Operator {\n  const alias = aliases.get(operator);\n\n  if (alias !== undefined) {\n    operator = alias;\n  }\n\n  if (basicOperators.has(operator)) {\n    return normalizeBasicOperatorForValue(operator, value, {query});\n  }\n\n  if (stringOperators.has(operator)) {\n    return normalizeStringOperatorForValue(operator, value, {query});\n  }\n\n  if (arrayOperators.has(operator)) {\n    return normalizeArrayOperatorForValue(operator, value, {query});\n  }\n\n  if (logicalOperators.has(operator)) {\n    return normalizeLogicalOperatorForValue(operator, value, {query});\n  }\n\n  throw new Error(\n    `A query contains an operator that is not supported (operator: '${operator}', query: '${JSON.stringify(\n      query\n    )}')`\n  );\n}\n\nfunction normalizeBasicOperatorForValue(\n  operator: Operator,\n  value: unknown,\n  {query}: {query: Query}\n): Operator {\n  if (operator === '$in') {\n    if (!Array.isArray(value)) {\n      throw new Error(\n        `Expected an array as value of the operator '${operator}', but received a value of type '${getTypeOf(\n          value\n        )}' (query: '${JSON.stringify(query)}')`\n      );\n    }\n\n    return operator;\n  }\n\n  if (typeof value === 'object' && !(value === null || value instanceof Date)) {\n    throw new Error(\n      `Expected a scalar value of the operator '${operator}', but received a value of type '${getTypeOf(\n        value\n      )}' (query: '${JSON.stringify(query)}')`\n    );\n  }\n\n  return operator;\n}\n\nfunction normalizeStringOperatorForValue(\n  operator: Operator,\n  value: unknown,\n  {query}: {query: Query}\n): Operator {\n  if (operator === '$matches') {\n    if (!(value instanceof RegExp)) {\n      throw new Error(\n        `Expected a regular expression as value of the operator '${operator}', but received a value of type '${getTypeOf(\n          value\n        )}' (query: '${JSON.stringify(query)}')`\n      );\n    }\n\n    return operator;\n  }\n\n  if (typeof value !== 'string') {\n    throw new Error(\n      `Expected a string as value of the operator '${operator}', but received a value of type '${getTypeOf(\n        value\n      )}' (query: '${JSON.stringify(query)}')`\n    );\n  }\n\n  return operator;\n}\n\nfunction normalizeArrayOperatorForValue(\n  operator: Operator,\n  value: unknown,\n  {query}: {query: Query}\n): Operator {\n  if (operator === '$length') {\n    if (typeof value !== 'number') {\n      throw new Error(\n        `Expected a number as value of the operator '${operator}', but received a value of type '${getTypeOf(\n          value\n        )}' (query: '${JSON.stringify(query)}')`\n      );\n    }\n\n    return operator;\n  }\n\n  return operator;\n}\n\nfunction normalizeLogicalOperatorForValue(\n  operator: Operator,\n  value: unknown,\n  {query}: {query: Query}\n): Operator {\n  if (operator === '$and' || operator === '$or' || operator === '$nor') {\n    if (!Array.isArray(value)) {\n      throw new Error(\n        `Expected an array as value of the operator '${operator}', but received a value of type '${getTypeOf(\n          value\n        )}' (query: '${JSON.stringify(query)}')`\n      );\n    }\n\n    return operator;\n  }\n\n  return operator;\n}\n"
  },
  {
    "path": "packages/storable/src/properties/index.ts",
    "content": "export * from './storable-attribute';\nexport * from './storable-method';\nexport * from './storable-primary-identifier-attribute';\nexport * from './storable-property';\nexport * from './storable-secondary-identifier-attribute';\n"
  },
  {
    "path": "packages/storable/src/properties/storable-attribute.test.ts",
    "content": "import {Component} from '@layr/component';\n\nimport {Storable} from '../storable';\nimport {StorableAttribute, isStorableAttributeInstance} from './storable-attribute';\n\ndescribe('StorableAttribute', () => {\n  test('new StorableAttribute()', async () => {\n    class Movie extends Storable(Component) {}\n\n    let loaderHasBeenCalled = false;\n    let finderHasBeenCalled = false;\n    let beforeLoadHasBeenCalled = false;\n\n    const storableAttribute = new StorableAttribute('title', Movie.prototype, {\n      valueType: 'string',\n      async loader() {\n        expect(this).toBe(Movie.prototype);\n        loaderHasBeenCalled = true;\n      },\n      async finder() {\n        expect(this).toBe(Movie.prototype);\n        finderHasBeenCalled = true;\n      },\n      async beforeLoad() {\n        expect(this).toBe(Movie.prototype);\n        beforeLoadHasBeenCalled = true;\n      }\n    });\n\n    expect(isStorableAttributeInstance(storableAttribute)).toBe(true);\n    expect(storableAttribute.getName()).toBe('title');\n    expect(storableAttribute.getParent()).toBe(Movie.prototype);\n\n    expect(storableAttribute.hasLoader()).toBe(true);\n    expect(storableAttribute.hasFinder()).toBe(true);\n    expect(storableAttribute.isComputed()).toBe(true);\n    expect(storableAttribute.hasHook('beforeLoad')).toBe(true);\n\n    expect(loaderHasBeenCalled).toBe(false);\n    await storableAttribute.callLoader();\n    expect(loaderHasBeenCalled).toBe(true);\n\n    expect(finderHasBeenCalled).toBe(false);\n    await storableAttribute.callFinder(1);\n    expect(finderHasBeenCalled).toBe(true);\n\n    expect(beforeLoadHasBeenCalled).toBe(false);\n    await storableAttribute.callHook('beforeLoad');\n    expect(beforeLoadHasBeenCalled).toBe(true);\n\n    const otherStorableAttribute = new StorableAttribute('country', Movie.prototype, {\n      valueType: 'string'\n    });\n\n    expect(otherStorableAttribute.hasLoader()).toBe(false);\n    expect(otherStorableAttribute.hasFinder()).toBe(false);\n    expect(otherStorableAttribute.isComputed()).toBe(false);\n    await expect(otherStorableAttribute.callLoader()).rejects.toThrow(\n      \"Cannot call a loader that is missing (attribute: 'Movie.prototype.country')\"\n    );\n    await expect(otherStorableAttribute.callFinder(1)).rejects.toThrow(\n      \"Cannot call a finder that is missing (attribute: 'Movie.prototype.country')\"\n    );\n\n    expect(otherStorableAttribute.hasHook('beforeLoad')).toBe(false);\n    await expect(otherStorableAttribute.callHook('beforeLoad')).rejects.toThrow(\n      \"Cannot call a hook that is missing (attribute: 'Movie.prototype.country', hook: 'beforeLoad')\"\n    );\n  });\n\n  test('Introspection', async () => {\n    class Movie extends Storable(Component) {}\n\n    expect(\n      new StorableAttribute('limit', Movie, {\n        valueType: 'number',\n        value: 100,\n        exposure: {get: true}\n      }).introspect()\n    ).toStrictEqual({\n      name: 'limit',\n      type: 'StorableAttribute',\n      valueType: 'number',\n      value: 100,\n      exposure: {get: true}\n    });\n  });\n\n  test('Unintrospection', async () => {\n    expect(\n      StorableAttribute.unintrospect({\n        name: 'limit',\n        type: 'StorableAttribute',\n        valueType: 'number',\n        value: 100,\n        exposure: {get: true}\n      })\n    ).toEqual({\n      name: 'limit',\n      options: {valueType: 'number', value: 100, exposure: {get: true}}\n    });\n  });\n});\n"
  },
  {
    "path": "packages/storable/src/properties/storable-attribute.ts",
    "content": "import {Attribute} from '@layr/component';\nimport type {Component, AttributeOptions} from '@layr/component';\nimport {PromiseLikeable, hasOwnProperty, Constructor} from 'core-helpers';\n\n// TODO: Find a way to remove this useless import\n// I did that to remove a TypeScript error in the generated declaration file\n// @ts-ignore\nimport type {Property} from '@layr/component';\n\nimport {StorablePropertyMixin, StorablePropertyOptions} from './storable-property';\nimport {assertIsStorableClassOrInstance} from '../utilities';\n\nexport type StorableAttributeOptions = StorablePropertyOptions &\n  AttributeOptions & {\n    loader?: StorableAttributeLoader;\n    beforeLoad?: StorableAttributeHook;\n    afterLoad?: StorableAttributeHook;\n    beforeSave?: StorableAttributeHook;\n    afterSave?: StorableAttributeHook;\n    beforeDelete?: StorableAttributeHook;\n    afterDelete?: StorableAttributeHook;\n  };\n\nexport type StorableAttributeLoader = () => PromiseLikeable<unknown>;\n\nexport type StorableAttributeHook = (attribute: StorableAttribute) => PromiseLikeable<void>;\n\nexport type StorableAttributeHookName =\n  | 'beforeLoad'\n  | 'afterLoad'\n  | 'beforeSave'\n  | 'afterSave'\n  | 'beforeDelete'\n  | 'afterDelete';\n\nexport const StorableAttributeMixin = <T extends Constructor<typeof Attribute>>(Base: T) =>\n  /**\n   * @name StorableAttribute\n   *\n   * *Inherits from [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) and [`StorableProperty`](https://layrjs.com/docs/v2/reference/storable-property).*\n   *\n   * The `StorableAttribute` class extends the [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) class with some capabilities such as [computed attributes](https://layrjs.com/docs/v2/reference/storable-attribute#computed-attributes) or [hooks](https://layrjs.com/docs/v2/reference/storable-attribute#hooks).\n   *\n   * #### Usage\n   *\n   * Typically, you create a `StorableAttribute` and associate it to a [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class) using the [`@attribute()`](https://layrjs.com/docs/v2/reference/storable#attribute-decorator) decorator.\n   *\n   * For example, here is how you would define a `Movie` component with some attributes:\n   *\n   * ```\n   * // JS\n   *\n   * import {Component} from '﹫layr/component';\n   * import {Storable, primaryIdentifier, attribute} from '﹫layr/storable';\n   *\n   * class Movie extends Storable(Component) {\n   *   ﹫primaryIdentifier() id;\n   *\n   *   ﹫attribute('string') title = '';\n   *\n   *   ﹫attribute('number') rating;\n   *\n   *   ﹫attribute('Date') releaseDate;\n   * }\n   * ```\n   *\n   * ```\n   * // TS\n   *\n   * import {Component} from '﹫layr/component';\n   * import {Storable, primaryIdentifier, attribute} from '﹫layr/storable';\n   *\n   * class Movie extends Storable(Component) {\n   *   ﹫primaryIdentifier() id!: string;\n   *\n   *   ﹫attribute('string') title = '';\n   *\n   *   ﹫attribute('number') rating!: number;\n   *\n   *   ﹫attribute('Date') releaseDate!: Date;\n   * }\n   * ```\n   *\n   * So far we've defined some storable attributes in the same way we would do with [regular attributes](https://layrjs.com/docs/v2/reference/attribute). The only difference is that we imported the [`@attribute()`](https://layrjs.com/docs/v2/reference/storable#attribute-decorator) decorator from `﹫layr/storable` instead of `﹫layr/component`.\n   *\n   * Let's now see how to take advantage of some capabilities that are unique to storable attributes.\n   *\n   * ##### Computed Attributes\n   *\n   * A computed attribute is a special kind of component attribute that computes its value when the component is loaded with a storable component method such as [`load()`](https://layrjs.com/docs/v2/reference/storable#load-instance-method), [`get()`](https://layrjs.com/docs/v2/reference/storable#get-class-method), or [`find()`](https://layrjs.com/docs/v2/reference/storable#find-class-method).\n   *\n   * The value of a computed attribute shouldn't be set manually, and is not persisted when you [save](https://layrjs.com/docs/v2/reference/storable#save-instance-method) a component to a store.\n   *\n   * ###### Loaders\n   *\n   * Use the [`@loader()`](https://layrjs.com/docs/v2/reference/storable#loader-decorator) decorator to specify the function that computes the value of a computed attribute.\n   *\n   * For example, let's define a `isTrending` computed attribute that determines its value according to the movie's `rating` and `releaseDate`:\n   *\n   * ```\n   * // JS\n   *\n   * import {loader} from '﹫layr/storable';\n   *\n   * class Movie extends Storable(Component) {\n   *   // ...\n   *\n   *   @loader(async function() {\n   *     await this.load({rating: true, releaseDate: true});\n   *\n   *     const ratingLimit = 7;\n   *     const releaseDateLimit = new Date(Date.now() - 864000000); // 10 days before\n   *\n   *     return this.rating >= ratingLimit && this.releaseDate >= releaseDateLimit;\n   *   })\n   *   @attribute('boolean')\n   *   isTrending;\n   * }\n   * ```\n   *\n   * ```\n   * // TS\n   *\n   * import {loader} from '﹫layr/storable';\n   *\n   * class Movie extends Storable(Component) {\n   *   // ...\n   *\n   *   @loader(async function(this: Movie) {\n   *     await this.load({rating: true, releaseDate: true});\n   *\n   *     const ratingLimit = 7;\n   *     const releaseDateLimit = new Date(Date.now() - 864000000); // 10 days before\n   *\n   *     return this.rating >= ratingLimit && this.releaseDate >= releaseDateLimit;\n   *   })\n   *   @attribute('boolean')\n   *   isTrending!: boolean;\n   * }\n   * ```\n   *\n   * Then, when we get a movie, we can get the `isTrending` computed attribute like any attribute:\n   *\n   * ```\n   * const movie = await Movie.get('abc123', {title: true, isTrending: true});\n   *\n   * movie.title; // => 'Inception'\n   * movie.isTrending; // => true (on July 16th, 2010)\n   * ```\n   *\n   * ###### Finders\n   *\n   * The best thing about computed attributes is that they can be used in a [`Query`](https://layrjs.com/docs/v2/reference/query) when you are [finding](https://layrjs.com/docs/v2/reference/storable#find-class-method) or [counting](https://layrjs.com/docs/v2/reference/storable#count-class-method) some storable components.\n   *\n   * To enable that, use the [`@finder()`](https://layrjs.com/docs/v2/reference/storable#finder-decorator) decorator, and specify a function that returns a [`Query`](https://layrjs.com/docs/v2/reference/query).\n   *\n   * For example, let's make our `isTrending` attribute searchable by adding a finder:\n   *\n   * ```\n   * // JS\n   *\n   * import {loader} from '﹫layr/storable';\n   *\n   * class Movie extends Storable(Component) {\n   *   // ...\n   *\n   *   @loader(\n   *     // ...\n   *   )\n   *   @finder(function (isTrending) {\n   *     const ratingLimit = 7;\n   *     const releaseDateLimit = new Date(Date.now() - 864000000); // 10 days before\n   *\n   *     if (isTrending) {\n   *       // Return a query for `{isTrending: true}`\n   *       return {\n   *         rating: {$greaterThanOrEqual: ratingLimit}\n   *         releaseDate: {$greaterThanOrEqual: releaseDateLimit}\n   *       };\n   *     }\n   *\n   *     // Return a query for `{isTrending: false}`\n   *     return {\n   *       $or: [\n   *         {rating: {$lessThan: ratingLimit}},\n   *         {releaseDate: {$lessThan: releaseDateLimit}}\n   *       ]\n   *     };\n   *   })\n   *   @attribute('boolean')\n   *   isTrending;\n   * }\n   * ```\n   *\n   * ```\n   * // TS\n   *\n   * import {loader} from '﹫layr/storable';\n   *\n   * class Movie extends Storable(Component) {\n   *   // ...\n   *\n   *   @loader(\n   *     // ...\n   *   )\n   *   @finder(function (isTrending: boolean) {\n   *     const ratingLimit = 7;\n   *     const releaseDateLimit = new Date(Date.now() - 864000000); // 10 days before\n   *\n   *     if (isTrending) {\n   *       // Return a query for `{isTrending: true}`\n   *       return {\n   *         rating: {$greaterThanOrEqual: ratingLimit}\n   *         releaseDate: {$greaterThanOrEqual: releaseDateLimit}\n   *       };\n   *     }\n   *\n   *     // Return a query for `{isTrending: false}`\n   *     return {\n   *       $or: [\n   *         {rating: {$lessThan: ratingLimit}},\n   *         {releaseDate: {$lessThan: releaseDateLimit}}\n   *       ]\n   *     };\n   *   })\n   *   @attribute('boolean')\n   *   isTrending!: boolean;\n   * }\n   * ```\n   *\n   * And now, we can query our `isTrending` computed attribute like we would do with any attribute:\n   *\n   * ```\n   * await Movie.find({isTrending: true}); // => All trending movies\n   * await Movie.find({isTrending: false}); // => All non-trending movies\n   *\n   * await Movie.count({isTrending: true}); // => Number of trending movies\n   * await Movie.count({isTrending: false}); // => Number of non-trending movies\n   *\n   * // Combine computed attributes with regular attributes to find\n   * // all Japanese trending movies\n   * await Movie.find({country: 'Japan', isTrending: true});\n   * ```\n   *\n   * ##### Hooks\n   *\n   * Storable attributes offer a number of hooks that you can use to execute some custom logic when an attribute is loaded, saved or deleted.\n   *\n   * To define a hook for a storable attribute, use one of the following [`@attribute()`](https://layrjs.com/docs/v2/reference/storable#attribute-decorator) options:\n   *\n   * - `beforeLoad`: Specifies a [`Hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) to be executed *before* an attribute is *loaded*.\n   * - `afterLoad`: Specifies a [`Hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) to be executed *after* an attribute is *loaded*.\n   * - `beforeSave`: Specifies a [`Hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) to be executed *before* an attribute is *saved*.\n   * - `afterSave`: Specifies a [`Hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) to be executed *after* an attribute is *saved*.\n   * - `beforeDelete`: Specifies a [`Hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) to be executed *before* an attribute is *deleted*.\n   * - `afterDelete`: Specifies a [`Hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) to be executed *after* an attribute is *deleted*.\n   *\n   * For example, we could use a `beforeSave` hook to make sure a user's password is hashed before it is saved to a store:\n   *\n   * ```\n   * // JS\n   *\n   * import {Component} from '﹫layr/component';\n   * import {Storable, primaryIdentifier, attribute} from '﹫layr/storable';\n   * import bcrypt from 'bcryptjs';\n   *\n   * class User extends Storable(Component) {\n   *   ﹫primaryIdentifier() id;\n   *\n   *   ﹫attribute('string') username;\n   *\n   *   ﹫attribute('string', {\n   *     async beforeSave() {\n   *       this.password = await bcrypt.hash(this.password);\n   *     }\n   *   })\n   *   password;\n   * }\n   * ```\n   *\n   * ```\n   * // TS\n   *\n   * import {Component} from '﹫layr/component';\n   * import {Storable, primaryIdentifier, attribute} from '﹫layr/storable';\n   * import bcrypt from 'bcryptjs';\n   *\n   * class User extends Storable(Component) {\n   *   ﹫primaryIdentifier() id!: string;\n   *\n   *   ﹫attribute('string') username!: string;\n   *\n   *   ﹫attribute('string', {\n   *     async beforeSave(this: User) {\n   *       this.password = await bcrypt.hash(this.password);\n   *     }\n   *   })\n   *   password!: string;\n   * }\n   * ```\n   *\n   * Then, when we save a user, its password gets automatically hashed:\n   * ```\n   * const user = new User({username: 'steve', password: 'zyx98765'});\n   *\n   * user.password; // => 'zyx98765'\n   *\n   * await user.save(); // The password will be hashed before saved to the store\n   *\n   * user.password; // => '$2y$12$AGJ91pnqlM7TcqnLg0iIFuiN80z9k.wFnGVl1a4lrANUepBKmvNVO'\n   *\n   * // Note that if we save the user again, as long as its password hasn't changed,\n   * // it will not be saved, and therefore not be hashed again\n   *\n   * user.username = 'steve2';\n   * await user.save(); // Only the username will be saved\n   *\n   * user.password; // => '$2y$12$AGJ91pnqlM7TcqnLg0iIFuiN80z9k.wFnGVl1a4lrANUepBKmvNVO'\n   * ```\n   */\n  class extends StorablePropertyMixin(Base) {\n    /**\n     * @constructor\n     *\n     * Creates a storable attribute. Typically, instead of using this constructor, you would rather use the [`@attribute()`](https://layrjs.com/docs/v2/reference/storable#attribute-decorator) decorator.\n     *\n     * @param name The name of the attribute.\n     * @param parent The [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class) class, prototype, or instance that owns the attribute.\n     * @param [options.valueType] A string specifying the [type of values](https://layrjs.com/docs/v2/reference/value-type#supported-types) the attribute can store (default: `'any'`).\n     * @param [options.default] The default value (or a function returning the default value) of the attribute.\n     * @param [options.validators] An array of [validators](https://layrjs.com/docs/v2/reference/validator) for the value of the attribute.\n     * @param [options.items.validators] An array of [validators](https://layrjs.com/docs/v2/reference/validator) for the items of an array attribute.\n     * @param [options.loader] A function specifying a [`Loader`](https://layrjs.com/docs/v2/reference/storable-attribute#loader-type) for the attribute.\n     * @param [options.finder] A function specifying a [`Finder`](https://layrjs.com/docs/v2/reference/storable-property#finder-type) for the attribute.\n     * @param [options.beforeLoad] A function specifying a \"beforeLoad\" [`hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) for the attribute.\n     * @param [options.afterLoad] A function specifying an \"afterLoad\" [`hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) for the attribute.\n     * @param [options.beforeSave] A function specifying a \"beforeSave\" [`hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) for the attribute.\n     * @param [options.afterSave] A function specifying an \"afterSave\" [`hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) for the attribute.\n     * @param [options.beforeDelete] A function specifying a \"beforeDelete\" [`hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) for the attribute.\n     * @param [options.afterDelete] A function specifying an \"afterDelete\" [`hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) for the attribute.\n     * @param [options.exposure] A [`PropertyExposure`](https://layrjs.com/docs/v2/reference/property#property-exposure-type) object specifying how the attribute should be exposed to remote access.\n     *\n     * @returns The [`StorableAttribute`](https://layrjs.com/docs/v2/reference/storable-attribute) instance that was created.\n     *\n     * @category Creation\n     */\n\n    // === Options ===\n\n    setOptions(options: StorableAttributeOptions = {}) {\n      const {\n        loader,\n        beforeLoad,\n        afterLoad,\n        beforeSave,\n        afterSave,\n        beforeDelete,\n        afterDelete,\n        ...otherOptions\n      } = options;\n\n      if (loader !== undefined) {\n        this.setLoader(loader);\n      }\n\n      if (beforeLoad !== undefined) {\n        this.setHook('beforeLoad', beforeLoad);\n      }\n\n      if (afterLoad !== undefined) {\n        this.setHook('afterLoad', afterLoad);\n      }\n\n      if (beforeSave !== undefined) {\n        this.setHook('beforeSave', beforeSave);\n      }\n\n      if (afterSave !== undefined) {\n        this.setHook('afterSave', afterSave);\n      }\n\n      if (beforeDelete !== undefined) {\n        this.setHook('beforeDelete', beforeDelete);\n      }\n\n      if (afterDelete !== undefined) {\n        this.setHook('afterDelete', afterDelete);\n      }\n\n      super.setOptions(otherOptions);\n    }\n\n    // === 'isControlled' mark\n\n    isControlled() {\n      return super.isControlled() || this.isComputed();\n    }\n\n    // === Property Methods ===\n\n    /**\n     * See the methods that are inherited from the [`Property`](https://layrjs.com/docs/v2/reference/property#basic-methods) class.\n     *\n     * @category Property Methods\n     */\n\n    // === Attribute Methods ===\n\n    /**\n     * See the methods that are inherited from the [`Attribute`](https://layrjs.com/docs/v2/reference/attribute#value-type) class.\n     *\n     * @category Attribute Methods\n     */\n\n    // === Loader ===\n\n    _loader: StorableAttributeLoader | undefined;\n\n    /**\n     * Returns the [`Loader`](https://layrjs.com/docs/v2/reference/storable-attribute#loader-type) of the attribute.\n     *\n     * @returns A [`Loader`](https://layrjs.com/docs/v2/reference/storable-attribute#loader-type) function (or `undefined` if the attribute has no associated loader).\n     *\n     * @category Loader\n     */\n    getLoader() {\n      return this._loader;\n    }\n\n    /**\n     * Returns whether the attribute has a [`Loader`](https://layrjs.com/docs/v2/reference/storable-attribute#loader-type).\n     *\n     * @returns A boolean.\n     *\n     * @category Loader\n     */\n    hasLoader() {\n      return this.getLoader() !== undefined;\n    }\n\n    /**\n     * Sets a [`Loader`](https://layrjs.com/docs/v2/reference/storable-attribute#loader-type) for the attribute.\n     *\n     * @param loader The [`Loader`](https://layrjs.com/docs/v2/reference/storable-attribute#loader-type) function to set.\n     *\n     * @category Loader\n     */\n    setLoader(loader: StorableAttributeLoader) {\n      this._loader = loader;\n    }\n\n    async callLoader() {\n      const loader = this.getLoader();\n\n      if (loader === undefined) {\n        throw new Error(`Cannot call a loader that is missing (${this.describe()})`);\n      }\n\n      return await loader.call(this.getParent());\n    }\n\n    /**\n     * @typedef Loader\n     *\n     * A function representing the \"loader\" of an attribute.\n     *\n     * The function should return a value for the attribute that is being loaded. Typically, you would return a value according to the value of some other attributes.\n     *\n     * The function can be `async` and is executed with the parent of the attribute as `this` context.\n     *\n     * See an example of use in the [\"Computed Attributes\"](https://layrjs.com/docs/v2/reference/storable-attribute#computed-attributes) section above.\n     *\n     * @category Loader\n     */\n\n    // === Finder ===\n\n    /**\n     * See the methods that are inherited from the [`StorableProperty`](https://layrjs.com/docs/v2/reference/storable-property#finder) class.\n     *\n     * @category Finder\n     */\n\n    isComputed() {\n      return this.hasLoader() || this.hasFinder();\n    }\n\n    // === Hooks ===\n\n    /**\n     * Returns a specific [`Hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) for the current attribute.\n     *\n     * @param name A string representing the name of the hook you want to get. The possible values are `'beforeLoad'`, `'afterLoad'`, `'beforeSave'`, `'afterSave'`, `'beforeDelete'`, and `'afterDelete'`.\n     *\n     * @returns A [`Hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) function (or `undefined` if the attribute doesn't have a hook with the specified `name`).\n     *\n     * @category Hooks\n     */\n    getHook(name: StorableAttributeHookName) {\n      return this._getHooks()[name];\n    }\n\n    /**\n     * Returns whether the current attribute has a specific [`Hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type).\n     *\n     * @param name A string representing the name of the hook you want to check. The possible values are `'beforeLoad'`, `'afterLoad'`, `'beforeSave'`, `'afterSave'`, `'beforeDelete'`, and `'afterDelete'`.\n     *\n     * @returns A boolean.\n     *\n     * @category Hooks\n     */\n    hasHook(name: StorableAttributeHookName) {\n      return name in this._getHooks();\n    }\n\n    /**\n     * Sets a [`Hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) for the current attribute.\n     *\n     * @param name A string representing the name of the hook you want to set. The possible values are `'beforeLoad'`, `'afterLoad'`, `'beforeSave'`, `'afterSave'`, `'beforeDelete'`, and `'afterDelete'`.\n     * @param hook The [`Hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) function to set.\n     *\n     * @category Hooks\n     */\n    setHook(name: StorableAttributeHookName, hook: StorableAttributeHook) {\n      this._getHooks(true)[name] = hook;\n    }\n\n    async callHook(name: StorableAttributeHookName) {\n      const hook = this.getHook(name);\n\n      if (hook === undefined) {\n        throw new Error(`Cannot call a hook that is missing (${this.describe()}, hook: '${name}')`);\n      }\n\n      await hook.call(this.getParent(), this);\n    }\n\n    _hooks!: Partial<Record<StorableAttributeHookName, StorableAttributeHook>>;\n\n    _getHooks(autoFork = false) {\n      if (this._hooks === undefined) {\n        Object.defineProperty(this, '_hooks', {\n          value: Object.create(null)\n        });\n      } else if (autoFork && !hasOwnProperty(this, '_hooks')) {\n        Object.defineProperty(this, '_hooks', {\n          value: Object.create(this._hooks)\n        });\n      }\n\n      return this._hooks;\n    }\n\n    /**\n     * @typedef Hook\n     *\n     * A function representing a \"hook\" of an attribute.\n     *\n     * According to the type of the hook, the function is automatically called when an attribute is loaded, saved or deleted.\n     *\n     * The function can be `async` and is invoked with the attribute as first parameter and the parent of the attribute (i.e., the storable component) as `this` context.\n     *\n     * See an example of use in the [\"Hooks\"](https://layrjs.com/docs/v2/reference/storable-attribute#hooks) section above.\n     *\n     * @category Hooks\n     */\n\n    // === Observability ===\n\n    /**\n     * See the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) class.\n     *\n     * @category Observability\n     */\n\n    // === Utilities ===\n\n    static isStorableAttribute(value: any): value is StorableAttribute {\n      return isStorableAttributeInstance(value);\n    }\n  };\n\nexport function isStorableAttributeClass(value: any): value is typeof StorableAttribute {\n  return typeof value?.isStorableAttribute === 'function';\n}\n\nexport function isStorableAttributeInstance(value: any): value is StorableAttribute {\n  return isStorableAttributeClass(value?.constructor) === true;\n}\n\nexport class StorableAttribute extends StorableAttributeMixin(Attribute) {\n  constructor(\n    name: string,\n    parent: typeof Component | Component,\n    options: StorableAttributeOptions = {}\n  ) {\n    assertIsStorableClassOrInstance(parent);\n\n    super(name, parent, options);\n  }\n}\n"
  },
  {
    "path": "packages/storable/src/properties/storable-method.ts",
    "content": "import {Method} from '@layr/component';\nimport type {Component, MethodOptions} from '@layr/component';\nimport {Constructor} from 'core-helpers';\n\n// TODO: Find a way to remove this useless import\n// I did that to remove a TypeScript error in the generated declaration file\n// @ts-ignore\nimport type {Property} from '@layr/component';\n\nimport {StorablePropertyMixin, StorablePropertyOptions} from './storable-property';\nimport {assertIsStorableClassOrInstance} from '../utilities';\n\nexport type StorableMethodOptions = StorablePropertyOptions & MethodOptions;\n\nexport const StorableMethodMixin = <T extends Constructor<typeof Method>>(Base: T) =>\n  /**\n   * @name StorableMethod\n   *\n   * *Inherits from [`Method`](https://layrjs.com/docs/v2/reference/method) and [`StorableProperty`](https://layrjs.com/docs/v2/reference/storable-property).*\n   *\n   * The `StorableMethod` class extends the [`Method`](https://layrjs.com/docs/v2/reference/method) class with the capabilities of the [`StorableProperty`](https://layrjs.com/docs/v2/reference/storable-property) class.\n   *\n   * In a nutshell, using the `StorableMethod` class allows you to associate a [`Finder`](https://layrjs.com/docs/v2/reference/storable-property#finder-type) to a method so this method can be used in a [`Query`](https://layrjs.com/docs/v2/reference/query).\n   *\n   *\n   * #### Usage\n   *\n   * Typically, you create a `StorableMethod` and associate it to a [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class) using the [`@method()`](https://layrjs.com/docs/v2/reference/storable#method-decorator) decorator.\n   *\n   * For example, here is how you would define a `Movie` component with some storable attributes and methods:\n   *\n   * ```\n   * // JS\n   *\n   * import {Component} from '﹫layr/component';\n   * import {Storable, primaryIdentifier, attribute, method} from '﹫layr/storable';\n   *\n   * class Movie extends Storable(Component) {\n   *   ﹫primaryIdentifier() id;\n   *\n   *   ﹫attribute('string') title = '';\n   *\n   *   ﹫attribute('string') country = '';\n   *\n   *   ﹫attribute('Date') releaseDate;\n   *\n   *   ﹫method() async wasReleasedIn(year) {\n   *     await this.load({releaseDate});\n   *\n   *     return this.releaseDate().getFullYear() === year;\n   *   }\n   * }\n   * ```\n   *\n   * ```\n   * // TS\n   *\n   * import {Component} from '﹫layr/component';\n   * import {Storable, primaryIdentifier, attribute, method} from '﹫layr/storable';\n   *\n   * class Movie extends Storable(Component) {\n   *   ﹫primaryIdentifier() id!: string;\n   *\n   *   ﹫attribute('string') title = '';\n   *\n   *   ﹫attribute('string') country = '';\n   *\n   *   ﹫attribute('Date') releaseDate!: Date;\n   *\n   *   ﹫method() async wasReleasedIn(year: number) {\n   *     await this.load({releaseDate});\n   *\n   *     return this.releaseDate().getUTCFullYear() === year;\n   *   }\n   * }\n   * ```\n   *\n   * Notice the `wasReleasedIn()` method that allows us to determine if a movie was released in a specific year. We could use this method as follows:\n   *\n   * ```\n   * const movie = new Movie({\n   *   title: 'Inception',\n   *   country: 'USA',\n   *   releaseDate: new Date('2010-07-16')\n   * });\n   *\n   * await movie.wasReleasedIn(2010); // => true\n   * await movie.wasReleasedIn(2011); // => false\n   * ```\n   *\n   * So far, there is nothing special about the `wasReleasedIn()` method. We could have achieved the same result without the [`@method()`](https://layrjs.com/docs/v2/reference/storable#method-decorator) decorator.\n   *\n   * Now, let's imagine that we want to find all the movies that was released in 2010. We could do so as follows:\n   *\n   * ```\n   * await Movie.find({\n   *   releaseDate: {\n   *     $greaterThanOrEqual: new Date('2010-01-01'),\n   *     $lessThan: new Date('2011-01-01')\n   *   }\n   * });\n   * ```\n   *\n   * That would certainly work, but wouldn't it be great if we could do the following instead:\n   *\n   * ```\n   * await Movie.find({wasReleasedIn: 2010});\n   * ```\n   *\n   * Unfortunately, the above [`Query`](https://layrjs.com/docs/v2/reference/query) wouldn't work. To make such a query possible, we must somehow transform the logic of the `wasReleasedIn()` method into a regular query, and this is exactly where a `StorableMethod` can be useful.\n   *\n   * Because the `wasReleasedIn()` method is a `StorableMethod` (thanks to the [`@method()`](https://layrjs.com/docs/v2/reference/storable#method-decorator) decorator), we can can associate a [`Finder`](https://layrjs.com/docs/v2/reference/storable-property#finder-type) to it by adding the [`@finder()`](https://layrjs.com/docs/v2/reference/storable#finder-decorator) decorator:\n   *\n   * ```\n   * // JS\n   *\n   * // ...\n   *\n   * import {finder} from '﹫layr/storable';\n   *\n   * class Movie extends Storable(Component) {\n   *   // ...\n   *\n   *   ﹫finder(function (year) {\n   *     return {\n   *       releaseDate: {\n   *         $greaterThanOrEqual: new Date(`${year}-01-01`),\n   *         $lessThan: new Date(`${year + 1}-01-01`)\n   *       }\n   *     };\n   *   })\n   *   ﹫method()\n   *   async wasReleasedIn(year) {\n   *     // ...\n   *   }\n   * }\n   * ```\n   *\n   * ```\n   * // TS\n   *\n   * // ...\n   *\n   * import {finder} from '﹫layr/storable';\n   *\n   * class Movie extends Storable(Component) {\n   *   // ...\n   *\n   *   ﹫finder(function (year: number) {\n   *     return {\n   *       releaseDate: {\n   *         $greaterThanOrEqual: new Date(`${year}-01-01`),\n   *         $lessThan: new Date(`${year + 1}-01-01`)\n   *       }\n   *     };\n   *   })\n   *   ﹫method()\n   *   async wasReleasedIn(year: number) {\n   *     // ...\n   *   }\n   * }\n   * ```\n   *\n   * And now, it is possible to use the `wasReleasedIn()` method in any query:\n   *\n   * ```\n   * // Find all the movies released in 2010\n   * await Movie.find({wasReleasedIn: 2010});\n   *\n   * // Find all the American movies released in 2010\n   * await Movie.find({country: 'USA', wasReleasedIn: 2010});\n   * ```\n   */\n  class extends StorablePropertyMixin(Base) {\n    _storableMethodBrand!: void;\n\n    /**\n     * @constructor\n     *\n     * Creates a storable method. Typically, instead of using this constructor, you would rather use the [`@method()`](https://layrjs.com/docs/v2/reference/storable#method-decorator) decorator.\n     *\n     * @param name The name of the method.\n     * @param parent The [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class) class, prototype, or instance that owns the method.\n     * @param [options] An object specifying any option supported by the constructor of [`Method`](https://layrjs.com/docs/v2/reference/method#constructor) and [`StorableProperty`](https://layrjs.com/docs/v2/reference/storable-property#constructor).\n     *\n     * @returns The [`StorableMethod`](https://layrjs.com/docs/v2/reference/storable-method) instance that was created.\n     *\n     * @category Creation\n     */\n\n    // === Property Methods ===\n\n    /**\n     * See the methods that are inherited from the [`Property`](https://layrjs.com/docs/v2/reference/property#basic-methods) class.\n     *\n     * @category Property Methods\n     */\n\n    // === Finder ===\n\n    /**\n     * See the methods that are inherited from the [`StorableProperty`](https://layrjs.com/docs/v2/reference/storable-property#finder) class.\n     *\n     * @category Finder\n     */\n\n    // === Utilities ===\n\n    static isStorableMethod(value: any): value is StorableMethod {\n      return isStorableMethodInstance(value);\n    }\n  };\n\nexport function isStorableMethodClass(value: any): value is typeof StorableMethod {\n  return typeof value?.isStorableMethod === 'function';\n}\n\nexport function isStorableMethodInstance(value: any): value is StorableMethod {\n  return isStorableMethodClass(value?.constructor) === true;\n}\n\nexport class StorableMethod extends StorableMethodMixin(Method) {\n  constructor(\n    name: string,\n    parent: typeof Component | Component,\n    options: StorableMethodOptions = {}\n  ) {\n    assertIsStorableClassOrInstance(parent);\n\n    super(name, parent, options);\n  }\n}\n"
  },
  {
    "path": "packages/storable/src/properties/storable-primary-identifier-attribute.ts",
    "content": "import {PrimaryIdentifierAttribute} from '@layr/component';\n\n// TODO: Find a way to remove this useless import\n// I did that to remove a TypeScript error in the generated declaration file\n// @ts-ignore\nimport type {Property, Attribute} from '@layr/component';\n\nimport {StorableAttributeMixin} from './storable-attribute';\n\n/**\n * *Inherits from [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute) and [`StorableAttribute`](https://layrjs.com/docs/v2/reference/storable-attribute).*\n *\n * The `StorablePrimaryIdentifierAttribute` class is like the [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute) class but extended with the capabilities of the [`StorableAttribute`](https://layrjs.com/docs/v2/reference/storable-attribute) class.\n *\n * #### Usage\n *\n * Typically, you create a `StorablePrimaryIdentifierAttribute` and associate it to a [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class) using the [`@primaryIdentifier()`](https://layrjs.com/docs/v2/reference/storable#primary-identifier-decorator) decorator.\n *\n * **Example:**\n *\n * ```\n * // JS\n *\n * import {Component} from '﹫layr/component';\n * import {Storable, primaryIdentifier, attribute} from '﹫layr/storable';\n *\n * class Movie extends Storable(Component) {\n *   ﹫primaryIdentifier() id;\n *\n *   ﹫attribute('string') title = '';\n * }\n * ```\n *\n * ```\n * // TS\n *\n * import {Component} from '﹫layr/component';\n * import {Storable, primaryIdentifier, attribute} from '﹫layr/storable';\n *\n * class Movie extends Storable(Component) {\n *   ﹫primaryIdentifier() id!: string;\n *\n *   ﹫attribute('string') title = '';\n * }\n * ```\n */\nexport class StorablePrimaryIdentifierAttribute extends StorableAttributeMixin(\n  PrimaryIdentifierAttribute\n) {\n  _storablePrimaryIdentifierAttributeBrand!: void;\n\n  /**\n   * @constructor\n   *\n   * Creates a storable primary identifier attribute. Typically, instead of using this constructor, you would rather use the [`@primaryIdentifier()`](https://layrjs.com/docs/v2/reference/storable#primary-identifier-decorator) decorator.\n   *\n   * @param name The name of the attribute.\n   * @param parent The [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class) prototype that owns the attribute.\n   * @param [options] An object specifying any option supported by the constructor of [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute#constructor) and [`StorableAttribute`](https://layrjs.com/docs/v2/reference/storable-attribute#constructor).\n   *\n   * @returns The [`StorablePrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/storable-primary-identifier-attribute) instance that was created.\n   *\n   * @category Creation\n   */\n\n  // === Property Methods ===\n\n  /**\n   * See the methods that are inherited from the [`Property`](https://layrjs.com/docs/v2/reference/property#basic-methods) class.\n   *\n   * @category Property Methods\n   */\n\n  // === Attribute Methods ===\n\n  /**\n   * See the methods that are inherited from the [`Attribute`](https://layrjs.com/docs/v2/reference/attribute#value-type) class.\n   *\n   * @category Attribute Methods\n   */\n}\n"
  },
  {
    "path": "packages/storable/src/properties/storable-property.ts",
    "content": "import {Property} from '@layr/component';\nimport type {Component, PropertyOptions} from '@layr/component';\nimport {PromiseLikeable, Constructor} from 'core-helpers';\n\nimport type {Query} from '../query';\nimport {assertIsStorableClassOrInstance} from '../utilities';\n\nexport type StorablePropertyOptions = PropertyOptions & {\n  finder?: StorablePropertyFinder;\n};\n\nexport type StorablePropertyFinder = (value: unknown) => PromiseLikeable<Query>;\n\nexport const StorablePropertyMixin = <T extends Constructor<typeof Property>>(Base: T) =>\n  /**\n   * @name StorableProperty\n   *\n   * *Inherits from [`Property`](https://layrjs.com/docs/v2/reference/property).*\n   *\n   * A base class from which classes such as [`StorableAttribute`](https://layrjs.com/docs/v2/reference/storable-attribute) or [`StorableMethod`](https://layrjs.com/docs/v2/reference/storable-method) are constructed. Unless you build a custom property class, you probably won't have to use this class directly.\n   */\n  class extends Base {\n    /**\n     * @constructor\n     *\n     * Creates a storable property.\n     *\n     * @param name The name of the property.\n     * @param parent The [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class) class, prototype, or instance that owns the property.\n     * @param [options.finder] A function specifying a [`Finder`](https://layrjs.com/docs/v2/reference/storable-property#finder-type) for the property.\n     * @param [options.exposure] A [`PropertyExposure`](https://layrjs.com/docs/v2/reference/property#property-exposure-type) object specifying how the property should be exposed to remote access.\n     *\n     * @returns The [`StorableProperty`](https://layrjs.com/docs/v2/reference/storable-property) instance that was created.\n     *\n     * @category Creation\n     */\n\n    // === Options ===\n\n    setOptions(options: StorablePropertyOptions = {}) {\n      const {finder, ...otherOptions} = options;\n\n      this._finder = finder;\n\n      super.setOptions(otherOptions);\n    }\n\n    // === Property Methods ===\n\n    /**\n     * See the methods that are inherited from the [`Property`](https://layrjs.com/docs/v2/reference/property#basic-methods) class.\n     *\n     * @category Property Methods\n     */\n\n    // === Finder ===\n\n    _finder: StorablePropertyFinder | undefined;\n\n    /**\n     * Returns the [`Finder`](https://layrjs.com/docs/v2/reference/storable-property#finder-type)of  the property.\n     *\n     * @returns A [`Finder`](https://layrjs.com/docs/v2/reference/storable-property#finder-type) function (or `undefined` if the property has no associated finder).\n     *\n     * @category Finder\n     */\n    getFinder() {\n      return this._finder;\n    }\n\n    /**\n     * Returns whether the property has a [`Finder`](https://layrjs.com/docs/v2/reference/storable-property#finder-type).\n     *\n     * @returns A boolean.\n     *\n     * @category Finder\n     */\n    hasFinder() {\n      return this.getFinder() !== undefined;\n    }\n\n    /**\n     * Sets a [`Finder`](https://layrjs.com/docs/v2/reference/storable-property#finder-type) for the property.\n     *\n     * @param finder The [`Finder`](https://layrjs.com/docs/v2/reference/storable-property#finder-type) function to set.\n     *\n     * @category Finder\n     */\n    setFinder(finder: StorablePropertyFinder) {\n      this._finder = finder;\n    }\n\n    async callFinder(value: unknown) {\n      const finder = this.getFinder();\n\n      if (finder === undefined) {\n        throw new Error(`Cannot call a finder that is missing (${this.describe()})`);\n      }\n\n      return await finder.call(this.getParent(), value);\n    }\n\n    /**\n     * @typedef Finder\n     *\n     * A function representing the \"finder\" of a property.\n     *\n     * The function should return a [`Query`](https://layrjs.com/docs/v2/reference/query) for the property that is queried for.\n     *\n     * The function has the following characteristics:\n     *\n     * - It can be `async`.\n     * - As first parameter, it receives the value that was specified in the user's query.\n     * - It is executed with the parent of the property as `this` context.\n     *\n     * See an example of use in the [`StorableAttribute`](https://layrjs.com/docs/v2/reference/storable-attribute) and [`StorableMethod`](https://layrjs.com/docs/v2/reference/storable-method) classes.\n     *\n     * @category Finder\n     */\n\n    // === Utilities ===\n\n    static isStorableProperty(value: any): value is StorableProperty {\n      return isStorablePropertyInstance(value);\n    }\n  };\n\nexport function isStorablePropertyClass(value: any): value is typeof StorableProperty {\n  return typeof value?.isStorableProperty === 'function';\n}\n\nexport function isStorablePropertyInstance(value: any): value is StorableProperty {\n  return isStorablePropertyClass(value?.constructor) === true;\n}\n\nexport class StorableProperty extends StorablePropertyMixin(Property) {\n  constructor(\n    name: string,\n    parent: typeof Component | Component,\n    options: StorablePropertyOptions = {}\n  ) {\n    assertIsStorableClassOrInstance(parent);\n\n    super(name, parent, options);\n  }\n}\n"
  },
  {
    "path": "packages/storable/src/properties/storable-secondary-identifier-attribute.ts",
    "content": "import {SecondaryIdentifierAttribute} from '@layr/component';\n\n// TODO: Find a way to remove this useless import\n// I did that to remove a TypeScript error in the generated declaration file\n// @ts-ignore\nimport type {Property, Attribute} from '@layr/component';\n\nimport {StorableAttributeMixin} from './storable-attribute';\n\n/**\n * *Inherits from [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute) and [`StorableAttribute`](https://layrjs.com/docs/v2/reference/storable-attribute).*\n *\n * The `StorableSecondaryIdentifierAttribute` class is like the [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute) class but extended with the capabilities of the [`StorableAttribute`](https://layrjs.com/docs/v2/reference/storable-attribute) class.\n *\n * #### Usage\n *\n * Typically, you create a `StorableSecondaryIdentifierAttribute` and associate it to a [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class) using the [`@secondaryIdentifier()`](https://layrjs.com/docs/v2/reference/storable#secondary-identifier-decorator) decorator.\n *\n * **Example:**\n *\n * ```\n * // JS\n *\n * import {Component} from '﹫layr/component';\n * import {Storable, primaryIdentifier, secondaryIdentifier, attribute} from '﹫layr/storable';\n *\n * class Movie extends Storable(Component) {\n *   ﹫primaryIdentifier() id;\n *\n *   ﹫secondaryIdentifier() slug;\n *\n *   ﹫attribute('string') title = '';\n * }\n * ```\n *\n * ```\n * // TS\n *\n * import {Component} from '﹫layr/component';\n * import {Storable, primaryIdentifier, secondaryIdentifier, attribute} from '﹫layr/storable';\n *\n * class Movie extends Storable(Component) {\n *   ﹫primaryIdentifier() id!: string;\n *\n *   ﹫secondaryIdentifier() slug!: string;\n *\n *   ﹫attribute('string') title = '';\n * }\n * ```\n */\nexport class StorableSecondaryIdentifierAttribute extends StorableAttributeMixin(\n  SecondaryIdentifierAttribute\n) {\n  _storableSecondaryIdentifierAttributeBrand!: void;\n\n  /**\n   * @constructor\n   *\n   * Creates a storable secondary identifier attribute. Typically, instead of using this constructor, you would rather use the [`@secondaryIdentifier()`](https://layrjs.com/docs/v2/reference/storable#secondary-identifier-decorator) decorator.\n   *\n   * @param name The name of the attribute.\n   * @param parent The [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class) prototype that owns the attribute.\n   * @param [options] An object specifying any option supported by the constructor of [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute#constructor) and [`StorableAttribute`](https://layrjs.com/docs/v2/reference/storable-attribute#constructor).\n   *\n   * @returns The [`StorableSecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/storable-secondary-identifier-attribute) instance that was created.\n   *\n   * @category Creation\n   */\n\n  // === Property Methods ===\n\n  /**\n   * See the methods that are inherited from the [`Property`](https://layrjs.com/docs/v2/reference/property#basic-methods) class.\n   *\n   * @category Property Methods\n   */\n\n  // === Attribute Methods ===\n\n  /**\n   * See the methods that are inherited from the [`Attribute`](https://layrjs.com/docs/v2/reference/attribute#value-type) class.\n   *\n   * @category Attribute Methods\n   */\n\n  // === Observability ===\n\n  /**\n   * See the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) class.\n   *\n   * @category Observability\n   */\n}\n"
  },
  {
    "path": "packages/storable/src/query.ts",
    "content": "import type {PlainObject} from 'core-helpers';\n\n/**\n * @typedef Query\n *\n * A plain object specifying the criteria to be used when selecting some components from a store with the methods [`StorableComponent.find()`](https://layrjs.com/docs/v2/reference/storable#find-class-method) or [`StorableComponent.count()`](https://layrjs.com/docs/v2/reference/storable#count-class-method).\n *\n * #### Basic Queries\n *\n * ##### Empty Query\n *\n * Specify an empty object (`{}`) to select all the components:\n *\n * ```\n * // Find all the movies\n * await Movie.find({});\n * ```\n *\n * ##### Single Attribute Query\n *\n * Specify an object composed of an attribute's name and value to select the components that have an attribute's value equals to a specific value:\n *\n * ```\n * // Find the Japanese movies\n * await Movie.find({country: 'Japan'});\n *\n * // Find the unreleased movies\n * await Movie.find({year: undefined});\n * ```\n *\n * ##### Multiple Attributes Query\n *\n * You can combine several attributes' name and value to select the components by multiple attributes:\n *\n * ```\n * // Find the Japanese drama movies\n * await Movie.find({country: 'Japan', genre: 'drama'});\n * ```\n *\n * #### Basic Operators\n *\n * Instead of a specific value, you can specify an object containing one or more operators to check whether the value of an attribute matches certain criteria.\n *\n * ##### `$equal`\n *\n * Use the `$equal` operator to check whether the value of an attribute is equal to a specific value. This is the default operator, so when you specify a value without any operator, the `$equal` operator is used under the hood:\n *\n * ```\n * // Find the Japanese movies\n * await Movie.find({country: {$equal: 'Japan'}});\n *\n * // Same as above, but in a short manner\n * await Movie.find({country: 'Japan'});\n * ```\n *\n * ##### `$notEqual`\n *\n * Use the `$notEqual` operator to check whether the value of an attribute is different than a specific value:\n *\n * ```\n * // Find the non-Japanese movies\n * await Movie.find({country: {$notEqual: 'Japan'}});\n\n * // Find the released movies\n * await Movie.find({year: {$notEqual: undefined}});\n * ```\n *\n * ##### `$greaterThan`\n *\n * Use the `$greaterThan` operator to check whether the value of an attribute is greater than a specific value:\n *\n * ```\n * // Find the movies released after 2010\n * await Movie.find({year: {$greaterThan: 2010}});\n * ```\n *\n * ##### `$greaterThanOrEqual`\n *\n * Use the `$greaterThanOrEqual` operator to check whether the value of an attribute is greater than or equal to a specific value:\n *\n * ```\n * // Find the movies released in or after 2010\n * await Movie.find({year: {$greaterThanOrEqual: 2010}});\n * ```\n *\n * ##### `$lessThan`\n *\n * Use the `$lessThan` operator to check whether the value of an attribute is less than a specific value:\n *\n * ```\n * // Find the movies released before 2010\n * await Movie.find({year: {$lessThan: 2010}});\n * ```\n *\n * ##### `$lessThanOrEqual`\n *\n * Use the `$lessThanOrEqual` operator to check whether the value of an attribute is less than or equal to a specific value:\n *\n * ```\n * // Find the movies released in or before 2010\n * await Movie.find({year: {$lessThanOrEqual: 2010}});\n * ```\n *\n * ##### `$in`\n *\n * Use the `$in` operator to check whether the value of an attribute is equal to any value in the specified array:\n *\n * ```\n * // Find the movies that are Japanese or French\n * await Movie.find({country: {$in: ['Japan', 'France']}});\n *\n * // Find the movies that have any of the specified identifiers\n * await Movie.find({id: {$in: ['abc123', 'abc456', 'abc789']}});\n * ```\n *\n * ##### Combining several operators\n *\n * You can combine several operators to check whether the value of an attribute matches several criteria:\n *\n * ```\n * // Find the movies released after 2010 and before 2015\n * await Movie.find({year: {$greaterThan: 2010, $lessThan: 2015}});\n * ```\n *\n * #### String Operators\n *\n * A number of operators are dedicated to string attributes.\n *\n * ##### `$includes`\n *\n * Use the `$includes` operator to check whether the value of a string attribute includes a specific string:\n *\n * ```\n * // Find the movies that have the string 'awesome' in their title\n * await Movie.find({title: {$includes: 'awesome'}});\n * ```\n *\n * ##### `$startsWith`\n *\n * Use the `$startsWith` operator to check whether the value of a string attribute starts with a specific string:\n *\n * ```\n * // Find the movies that have their title starting with the string 'awesome'\n * await Movie.find({title: {$startsWith: 'awesome'}});\n * ```\n *\n * ##### `$endsWith`\n *\n * Use the `$endsWith` operator to check whether the value of a string attribute ends with a specific string:\n *\n * ```\n * // Find the movies that have their title ending with the string 'awesome'\n * await Movie.find({title: {$endsWith: 'awesome'}});\n * ```\n *\n * ##### `$matches`\n *\n * Use the `$matches` operator to check whether the value of a string attribute matches the specified [regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions):\n *\n * ```\n * // Find the movies that have a number in their title\n * await Movie.find({title: {$matches: /\\d/}});\n * ```\n *\n * #### Array Operators\n *\n * A number of operators are dedicated to array attributes.\n *\n * ##### `$some`\n *\n * Use the `$some` operator to check whether an array attribute has an item equals to a specific value. This is the default operator for array attributes, so when you specify a value without any array operator, the `$some` operator is used under the hood:\n *\n * ```\n * // Find the movies that have the 'awesome' tag\n * await Movie.find({tags: {$some: 'awesome'}});\n *\n * // Same as above, but in a short manner\n * await Movie.find({tags: 'awesome'});\n * ```\n *\n * ##### `$every`\n *\n * Use the `$every` operator to check whether an array attribute has all its items equals to a specific value:\n *\n * ```\n * // Find the movies that have all their tags equal to 'awesome'\n * await Movie.find({tags: {$every: 'awesome'}});\n * ```\n *\n * ##### `$length`\n *\n * Use the `$length` operator to check whether an array attribute has a specific number of items:\n *\n * ```\n * // Find the movies that have three tags:\n * await Movie.find({tags: {$length: 3}});\n *\n * // Find the movies that don't have any tag:\n * await Movie.find({tags: {$length: 0}});\n * ```\n *\n * #### Logical Operators\n *\n * The logical operators allows you to combine several subqueries.\n *\n * ##### `$and`\n *\n * Use the `$and` operator to perform a logical **AND** operation on an array of subqueries and select the components that satisfy *all* the subqueries. Note that since **AND** is the implicit logical operation when you combine multiple attributes or operators, you will typically use the `$and` operator in combination with some other logical operators such as [`$or`](https://layrjs.com/docs/v2/reference/query#or) to avoid repetition.\n *\n * ```\n * // Find the Japanese drama movies\n * await Movie.find({$and: [{country: 'Japan'}, {genre: 'drama'}]});\n *\n * // Same as above, but in a short manner\n * await Movie.find({country: 'Japan', genre: 'drama'});\n *\n * // Find the movies released after 2010 and before 2015\n * await Movie.find({$and: [{year: {$greaterThan: 2010}}, {year: {$lessThan: 2015}}]});\n *\n * // Same as above, but in a short manner\n * await Movie.find({year: {$greaterThan: 2010, $lessThan: 2015}});\n *\n * // Find the Japanese movies released before 2010 or after 2015\n * await Movie.find({\n *   $and: [\n *     {country: 'Japan'},\n *     {$or: [{year: {$lessThan: 2010}}, {year: {$greaterThan: 2015}}]}\n *   ]\n * });\n *\n * // Same as above, but we have to repeat the country to remove the $and operator\n * await Movie.find({\n *   $or: [\n *     {country: 'Japan', year: {$lessThan: 2010}},\n *     {country: 'Japan', year: {$greaterThan: 2015}}\n *   ]\n * });\n * ```\n *\n * ##### `$or`\n *\n * Use the `$or` operator to perform a logical **OR** operation on an array of subqueries and select the components that satisfy *at least* one of the subqueries.\n *\n * ```\n * // Find the movies that are either Japanese or a drama\n * await Movie.find({$or: [{country: 'Japan', {genre: 'drama'}]});\n *\n * // Find the movies released before 2010 or after 2015\n * await Movie.find({$or: [{year: {$lessThan: 2010}}, {year: {$greaterThan: 2015}}]});\n * ```\n *\n * ##### `$nor`\n *\n * Use the `$nor` operator to perform a logical **NOR** operation on an array of subqueries and select the components that *fail all* the subqueries.\n *\n * ```\n * // Find the movies that are not Japanese and not a drama\n * await Movie.find({$nor: [{country: 'Japan', {genre: 'drama'}]});\n * ```\n *\n * ##### `$not`\n *\n * Use the `$not` operator to invert the effect of an operator.\n *\n * ```\n * // Find the non-Japanese movies\n * await Movie.find({country: {$not: {$equal: 'Japan'}}});\n *\n * // Same as above, but in a short manner\n * await Movie.find({country: {$notEqual: 'Japan'}});\n *\n * // Find the movies that was not released in or after 2010\n * await Movie.find({year: {$not: {$greaterThanOrEqual: 2010}}});\n *\n * // Same as above, but in a short manner\n * await Movie.find({year: {$lessThan: 2010}});\n * ```\n *\n * #### Embedded Components\n *\n * When a query involves an [embedded component](https://layrjs.com/docs/v2/reference/embedded-component), wrap the attributes of the embedded component in an object:\n *\n * ```\n * // Find the movies that have a '16:9' aspect ratio\n * await Movie.find({details: {aspectRatio: '16:9'}});\n *\n * // Find the movies that have a '16:9' aspect ratio and are longer than 2 hours\n * await Movie.find({details: {aspectRatio: '16:9', duration: {$greaterThan: 120}}});\n * ```\n *\n * #### Referenced Components\n *\n * To check whether a component holds a [reference to another component](https://layrjs.com/docs/v2/reference/component#referencing-components), you can specify an object representing the [primary identifier](https://layrjs.com/docs/v2/reference/primary-identifier-attribute) of the referenced component:\n *\n * ```\n * // Find the Tarantino's movies\n * const tarantino = await Director.get({slug: 'quentin-tarantino'});\n * await Movie.find({director: {id: tarantino.id}});\n * ```\n *\n * Wherever you can specify a primary identifier, you can specify a component instead. So, the example above can be shortened as follows:\n *\n * ```\n * // Find the Tarantino's movies in a short manner\n * const tarantino = await Director.get({slug: 'quentin-tarantino'});\n * await Movie.find({director: tarantino});\n * ```\n */\n\nexport type Query = PlainObject;\n"
  },
  {
    "path": "packages/storable/src/storable.ts",
    "content": "import {\n  Component,\n  isComponentClass,\n  isComponentInstance,\n  isComponentClassOrInstance,\n  isComponentValueTypeInstance,\n  Attribute,\n  ValueType,\n  isArrayValueTypeInstance,\n  AttributeSelector,\n  createAttributeSelectorFromAttributes,\n  attributeSelectorsAreEqual,\n  mergeAttributeSelectors,\n  removeFromAttributeSelector,\n  traverseAttributeSelector,\n  trimAttributeSelector,\n  normalizeAttributeSelector,\n  IdentifierDescriptor,\n  IdentifierValue,\n  method\n} from '@layr/component';\nimport {hasOwnProperty, isPrototypeOf, isPlainObject, getTypeOf, Constructor} from 'core-helpers';\nimport mapKeys from 'lodash/mapKeys';\n\nimport {\n  StorableProperty,\n  StorablePropertyOptions,\n  isStorablePropertyInstance,\n  StorableAttribute,\n  StorableAttributeOptions,\n  isStorableAttributeInstance,\n  StorablePrimaryIdentifierAttribute,\n  StorableSecondaryIdentifierAttribute,\n  StorableAttributeHookName,\n  StorableMethod,\n  StorableMethodOptions,\n  isStorableMethodInstance\n} from './properties';\nimport {Index, IndexAttributes, IndexOptions} from './index-class';\nimport type {Query} from './query';\nimport type {StoreLike} from './store-like';\nimport {\n  isStorableInstance,\n  isStorableClassOrInstance,\n  ensureStorableClass,\n  isStorable\n} from './utilities';\n\nexport type SortDescriptor = {[name: string]: SortDirection};\n\nexport type SortDirection = 'asc' | 'desc';\n\n/**\n * Extends a [`Component`](https://layrjs.com/docs/v2/reference/component) class with some storage capabilities.\n *\n * #### Usage\n *\n * The `Storable()` mixin can be used both in the backend and the frontend.\n *\n * ##### Backend Usage\n *\n * Call `Storable()` with a [`Component`](https://layrjs.com/docs/v2/reference/component) class to construct a [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storable-component-class) class that you can extend with your data model and business logic. Then, register this class into a store such as [`MongoDBStore`](https://layrjs.com/docs/v2/reference/mongodb-store) by using the [`registerStorable()`](https://layrjs.com/docs/v2/reference/store#register-storable-instance-method) method (or [`registerRootComponent()`](https://layrjs.com/docs/v2/reference/store#register-root-component-instance-method) to register several components at once).\n *\n * **Example:**\n *\n * ```js\n * // JS\n *\n * import {Component} from '@layr/component';\n * import {Storable, primaryIdentifier, attribute} from '@layr/storable';\n * import {MongoDBStore} from '@layr/mongodb-store';\n *\n * export class Movie extends Storable(Component) {\n *   @primaryIdentifier() id;\n *\n *   @attribute() title = '';\n * }\n *\n * const store = new MongoDBStore('mongodb://user:pass@host:port/db');\n *\n * store.registerStorable(Movie);\n * ```\n *\n * ```ts\n * // TS\n *\n * import {Component} from '@layr/component';\n * import {Storable, primaryIdentifier, attribute} from '@layr/storable';\n * import {MongoDBStore} from '@layr/mongodb-store';\n *\n * export class Movie extends Storable(Component) {\n *   @primaryIdentifier() id!: string;\n *\n *   @attribute() title = '';\n * }\n *\n * const store = new MongoDBStore('mongodb://user:pass@host:port/db');\n *\n * store.registerStorable(Movie);\n * ```\n *\n * Once you have a storable component registered into a store, you can use any method provided by the `Storable()` mixin to interact with the database:\n *\n * ```\n * const movie = new Movie({id: 'abc123', title: 'Inception'});\n *\n * // Save the movie to the database\n * await movie.save();\n *\n * // Retrieve the movie from the database\n * await Movie.get('abc123'); // => movie\n * ```\n *\n * ##### Frontend Usage\n *\n * Typically, you construct a storable component in the frontend by \"inheriting\" a storable component exposed by the backend. To accomplish that, you create a [`ComponentHTTPClient`](https://layrjs.com/docs/v2/reference/component-http-client), and then call the [`getComponent()`](https://layrjs.com/docs/v2/reference/component-http-client#get-component-instance-method) method to construct your frontend component.\n *\n * **Example:**\n *\n * ```\n * import {ComponentHTTPClient} from '@layr/component-http-client';\n * import {Storable} from '@layr/storable';\n *\n * (async () => {\n *   const client = new ComponentHTTPClient('https://...', {\n *     mixins: [Storable]\n *   });\n *\n *   const Movie = await client.getComponent();\n * })();\n * ```\n *\n * > Note that you have to pass the `Storable` mixin when you create a `ComponentHTTPClient` that is consuming a storable component.\n *\n * Once you have a storable component in the frontend, you can use any method that is exposed by the backend. For example, if the `Movie`'s [`save()`](https://layrjs.com/docs/v2/reference/storable#save-instance-method) method is exposed by the backend, you can call it from the frontend to add a new movie into the database:\n *\n * ```\n * const movie = new Movie({title: 'Inception 2'});\n *\n * await movie.save();\n * ```\n *\n * See the [\"Storing Data\"](https://layrjs.com/docs/v2/introduction/storing-data) tutorial for a comprehensive example using the `Storable()` mixin.\n *\n * ### StorableComponent <badge type=\"primary\">class</badge> {#storable-component-class}\n *\n * *Inherits from [`Component`](https://layrjs.com/docs/v2/reference/component).*\n *\n * A `StorableComponent` class is constructed by calling the `Storable()` mixin ([see above](https://layrjs.com/docs/v2/reference/storable#storable-mixin)).\n *\n * @mixin\n */\nexport function Storable<T extends Constructor<typeof Component>>(Base: T) {\n  if (!isComponentClass(Base)) {\n    throw new Error(\n      `The Storable mixin should be applied on a component class (received type: '${getTypeOf(\n        Base\n      )}')`\n    );\n  }\n\n  if (typeof (Base as any).isStorable === 'function') {\n    return Base as T & typeof Storable;\n  }\n\n  class Storable extends Base {\n    declare ['constructor']: typeof StorableComponent;\n\n    // === Component Methods ===\n\n    /**\n     * See the methods that are inherited from the [`Component`](https://layrjs.com/docs/v2/reference/component#creation) class.\n     *\n     * @category Component Methods\n     */\n\n    // === Store registration ===\n\n    static __store: StoreLike | undefined;\n\n    /**\n     * Returns the store in which the storable component is registered. If the storable component is not registered in a store, an error is thrown.\n     *\n     * @returns A [`Store`](https://layrjs.com/docs/v2/reference/store) instance.\n     *\n     * @example\n     * ```\n     * Movie.getStore(); // => store\n     * ```\n     *\n     * @category Store Registration\n     */\n    static getStore() {\n      const store = this.__store;\n\n      if (store === undefined) {\n        throw new Error(\n          `Cannot get the store of a storable component that is not registered (${this.describeComponent()})`\n        );\n      }\n\n      return store;\n    }\n\n    /**\n     * Returns whether the storable component is registered in a store.\n     *\n     * @returns A boolean.\n     *\n     * @example\n     * ```\n     * Movie.hasStore(); // => true\n     * ```\n     *\n     * @category Store Registration\n     */\n    static hasStore() {\n      return this.__store !== undefined;\n    }\n\n    static __setStore(store: StoreLike) {\n      Object.defineProperty(this, '__store', {value: store});\n    }\n\n    // === Storable properties ===\n\n    static getPropertyClass(type: string) {\n      if (type === 'StorableAttribute') {\n        return StorableAttribute;\n      }\n\n      if (type === 'StorablePrimaryIdentifierAttribute') {\n        return StorablePrimaryIdentifierAttribute;\n      }\n\n      if (type === 'StorableSecondaryIdentifierAttribute') {\n        return StorableSecondaryIdentifierAttribute;\n      }\n\n      if (type === 'StorableMethod') {\n        return StorableMethod;\n      }\n\n      return super.getPropertyClass(type);\n    }\n\n    static get getStorableProperty() {\n      return this.prototype.getStorableProperty;\n    }\n\n    getStorableProperty(name: string, options: {autoFork?: boolean} = {}) {\n      const {autoFork = true} = options;\n\n      const property = this.__getStorableProperty(name, {autoFork});\n\n      if (property === undefined) {\n        throw new Error(`The storable property '${name}' is missing (${this.describeComponent()})`);\n      }\n\n      return property;\n    }\n\n    static get hasStorableProperty() {\n      return this.prototype.hasStorableProperty;\n    }\n\n    hasStorableProperty(name: string) {\n      return this.__getStorableProperty(name, {autoFork: false}) !== undefined;\n    }\n\n    static get __getStorableProperty() {\n      return this.prototype.__getStorableProperty;\n    }\n\n    __getStorableProperty(name: string, options: {autoFork: boolean}) {\n      const {autoFork} = options;\n\n      const property = this.__getProperty(name, {autoFork});\n\n      if (property === undefined) {\n        return undefined;\n      }\n\n      if (!isStorablePropertyInstance(property)) {\n        throw new Error(\n          `A property with the specified name was found, but it is not a storable property (${property.describe()})`\n        );\n      }\n\n      return property;\n    }\n\n    static get setStorableProperty() {\n      return this.prototype.setStorableProperty;\n    }\n\n    setStorableProperty(name: string, propertyOptions: StorablePropertyOptions = {}) {\n      return this.setProperty(name, StorableProperty, propertyOptions);\n    }\n\n    getStorablePropertiesWithFinder() {\n      return this.getProperties<StorableProperty>({\n        filter: (property) => isStorablePropertyInstance(property) && property.hasFinder()\n      });\n    }\n\n    // === Storable attributes ===\n\n    static get getStorableAttribute() {\n      return this.prototype.getStorableAttribute;\n    }\n\n    getStorableAttribute(name: string, options: {autoFork?: boolean} = {}) {\n      const {autoFork = true} = options;\n\n      const attribute = this.__getStorableAttribute(name, {autoFork});\n\n      if (attribute === undefined) {\n        throw new Error(\n          `The storable attribute '${name}' is missing (${this.describeComponent()})`\n        );\n      }\n\n      return attribute;\n    }\n\n    static get hasStorableAttribute() {\n      return this.prototype.hasStorableAttribute;\n    }\n\n    hasStorableAttribute(name: string) {\n      return this.__getStorableAttribute(name, {autoFork: false}) !== undefined;\n    }\n\n    static get __getStorableAttribute() {\n      return this.prototype.__getStorableAttribute;\n    }\n\n    __getStorableAttribute(name: string, options: {autoFork: boolean}) {\n      const {autoFork} = options;\n\n      const property = this.__getProperty(name, {autoFork});\n\n      if (property === undefined) {\n        return undefined;\n      }\n\n      if (!isStorableAttributeInstance(property)) {\n        throw new Error(\n          `A property with the specified name was found, but it is not a storable attribute (${property.describe()})`\n        );\n      }\n\n      return property;\n    }\n\n    static get setStorableAttribute() {\n      return this.prototype.setStorableAttribute;\n    }\n\n    setStorableAttribute(name: string, attributeOptions: StorableAttributeOptions = {}) {\n      return this.setProperty(name, StorableAttribute, attributeOptions);\n    }\n\n    getStorableAttributesWithLoader(\n      options: {attributeSelector?: AttributeSelector; setAttributesOnly?: boolean} = {}\n    ) {\n      const {attributeSelector = true, setAttributesOnly = false} = options;\n\n      return this.getAttributes<StorableAttribute>({\n        filter: (attribute) => isStorableAttributeInstance(attribute) && attribute.hasLoader(),\n        attributeSelector,\n        setAttributesOnly\n      });\n    }\n\n    getStorableComputedAttributes(\n      options: {attributeSelector?: AttributeSelector; setAttributesOnly?: boolean} = {}\n    ) {\n      const {attributeSelector = true, setAttributesOnly = false} = options;\n\n      return this.getAttributes<StorableAttribute>({\n        filter: (attribute) => isStorableAttributeInstance(attribute) && attribute.isComputed(),\n        attributeSelector,\n        setAttributesOnly\n      });\n    }\n\n    getStorableAttributesWithHook(\n      name: StorableAttributeHookName,\n      options: {attributeSelector?: AttributeSelector; setAttributesOnly?: boolean} = {}\n    ) {\n      const {attributeSelector = true, setAttributesOnly = false} = options;\n\n      return this.getAttributes<StorableAttribute>({\n        filter: (attribute) => isStorableAttributeInstance(attribute) && attribute.hasHook(name),\n        attributeSelector,\n        setAttributesOnly\n      });\n    }\n\n    async __callStorableAttributeHooks(\n      name: StorableAttributeHookName,\n      {\n        attributeSelector,\n        setAttributesOnly\n      }: {attributeSelector: AttributeSelector; setAttributesOnly?: boolean}\n    ) {\n      for (const attribute of this.getStorableAttributesWithHook(name, {\n        attributeSelector,\n        setAttributesOnly\n      })) {\n        await attribute.callHook(name);\n      }\n    }\n\n    // === Indexes ===\n\n    getIndex(attributes: IndexAttributes, options: {autoFork?: boolean} = {}) {\n      const {autoFork = true} = options;\n\n      const index = this.__getIndex(attributes, {autoFork});\n\n      if (index === undefined) {\n        throw new Error(\n          `The index \\`${JSON.stringify(attributes)}\\` is missing (${this.describeComponent()})`\n        );\n      }\n\n      return index;\n    }\n\n    hasIndex(attributes: IndexAttributes) {\n      return this.__getIndex(attributes, {autoFork: false}) !== undefined;\n    }\n\n    __getIndex(attributes: IndexAttributes, options: {autoFork: boolean}) {\n      const {autoFork} = options;\n\n      const indexes = this.__getIndexes();\n      const key = Index._buildIndexKey(attributes);\n\n      let index = indexes[key];\n\n      if (index === undefined) {\n        return undefined;\n      }\n\n      if (autoFork && index.getParent() !== this) {\n        index = index.fork(this);\n        indexes[key] = index;\n      }\n\n      return index;\n    }\n\n    setIndex(attributes: IndexAttributes, options: IndexOptions = {}): Index {\n      let index = this.hasIndex(attributes) ? this.getIndex(attributes) : undefined;\n\n      if (index === undefined) {\n        index = new Index(attributes, this, options);\n        const indexes = this.__getIndexes();\n        const key = Index._buildIndexKey(attributes);\n        indexes[key] = index;\n      } else {\n        index.setOptions(options);\n      }\n\n      return index;\n    }\n\n    deleteIndex(attributes: IndexAttributes) {\n      const indexes = this.__getIndexes();\n      const key = Index._buildIndexKey(attributes);\n\n      if (!hasOwnProperty(indexes, key)) {\n        return false;\n      }\n\n      delete indexes[key];\n\n      return true;\n    }\n\n    getIndexes(\n      options: {\n        autoFork?: boolean;\n      } = {}\n    ) {\n      const {autoFork = true} = options;\n\n      const storable = this;\n\n      return {\n        *[Symbol.iterator]() {\n          const indexes = storable.__getIndexes({autoCreateOrFork: false});\n\n          if (indexes !== undefined) {\n            for (const key in indexes) {\n              const attributes = indexes[key].getAttributes();\n\n              const index = storable.getIndex(attributes, {autoFork});\n\n              yield index;\n            }\n          }\n        }\n      };\n    }\n\n    __indexes?: {[name: string]: Index};\n\n    __getIndexes({autoCreateOrFork = true} = {}) {\n      if (autoCreateOrFork) {\n        if (!('__indexes' in this)) {\n          Object.defineProperty(this, '__indexes', {value: Object.create(null)});\n        } else if (!hasOwnProperty(this, '__indexes')) {\n          Object.defineProperty(this, '__indexes', {value: Object.create(this.__indexes!)});\n        }\n      }\n\n      return this.__indexes!;\n    }\n\n    // === Storable methods ===\n\n    static get getStorableMethod() {\n      return this.prototype.getStorableMethod;\n    }\n\n    getStorableMethod(name: string, options: {autoFork?: boolean} = {}) {\n      const {autoFork = true} = options;\n\n      const method = this.__getStorableMethod(name, {autoFork});\n\n      if (method === undefined) {\n        throw new Error(`The storable method '${name}' is missing (${this.describeComponent()})`);\n      }\n\n      return method;\n    }\n\n    static get hasStorableMethod() {\n      return this.prototype.hasStorableMethod;\n    }\n\n    hasStorableMethod(name: string) {\n      return this.__getStorableMethod(name, {autoFork: false}) !== undefined;\n    }\n\n    static get __getStorableMethod() {\n      return this.prototype.__getStorableMethod;\n    }\n\n    __getStorableMethod(name: string, options: {autoFork: boolean}) {\n      const {autoFork} = options;\n\n      const property = this.__getProperty(name, {autoFork});\n\n      if (property === undefined) {\n        return undefined;\n      }\n\n      if (!isStorableMethodInstance(property)) {\n        throw new Error(\n          `A property with the specified name was found, but it is not a storable method (${property.describe()})`\n        );\n      }\n\n      return property;\n    }\n\n    static get setStorableMethod() {\n      return this.prototype.setStorableMethod;\n    }\n\n    setStorableMethod(name: string, methodOptions: StorableMethodOptions = {}) {\n      return this.setProperty(name, StorableMethod, methodOptions);\n    }\n\n    // === Operations ===\n\n    /**\n     * Retrieves a storable component instance (and possibly, some of its referenced components) from the store.\n     *\n     * > This method uses the [`load()`](https://layrjs.com/docs/v2/reference/storable#load-instance-method) method under the hood to load the component's attributes. So if you want to expose the [`get()`](https://layrjs.com/docs/v2/reference/storable#get-class-method) method to the frontend, you will typically have to expose the [`load()`](https://layrjs.com/docs/v2/reference/storable#load-instance-method) method as well.\n     *\n     * @param identifier A plain object specifying the identifier of the component you want to retrieve. The shape of the object should be `{[identifierName]: identifierValue}`. Alternatively, you can specify a string or a number representing the value of a [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute).\n     * @param [attributeSelector] An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be loaded (default: `true`, which means that all the attributes will be loaded).\n     * @param [options.reload] A boolean specifying whether a component that has already been loaded should be loaded again from the store (default: `false`). Most of the time you will leave this option off to take advantage of the cache.\n     * @param [options.throwIfMissing] A boolean specifying whether an error should be thrown if there is no component matching the specified `identifier` in the store (default: `true`).\n     *\n     * @returns A [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storable-component-class) instance.\n     *\n     * @example\n     * ```\n     * // Fully retrieve a movie by its primary identifier\n     * await Movie.get({id: 'abc123'});\n\n     * // Same as above, but in a short manner\n     * await Movie.get('abc123');\n     *\n     * // Fully retrieve a movie by its secondary identifier\n     * await Movie.get({slug: 'inception'});\n     *\n     * // Partially retrieve a movie by its primary identifier\n     * await Movie.get({id: 'abc123'}, {title: true, rating: true});\n     *\n     * // Partially retrieve a movie, and fully retrieve its referenced director component\n     * await Movie.get({id: 'abc123'}, {title: true, director: true});\n     *\n     * // Partially retrieve a movie, and partially retrieve its referenced director component\n     * await Movie.get({id: 'abc123'}, {title: true, director: {fullName: true}});\n     * ```\n     *\n     * @category Storage Operations\n     */\n    static async get<T extends typeof StorableComponent>(\n      this: T,\n      identifierDescriptor: IdentifierDescriptor,\n      attributeSelector: AttributeSelector | undefined,\n      options: {reload?: boolean; throwIfMissing: false; _callerMethodName?: string}\n    ): Promise<InstanceType<T> | undefined>;\n    static async get<T extends typeof StorableComponent>(\n      this: T,\n      identifierDescriptor: IdentifierDescriptor,\n      attributeSelector?: AttributeSelector,\n      options?: {reload?: boolean; throwIfMissing?: boolean; _callerMethodName?: string}\n    ): Promise<InstanceType<T>>;\n    @method() static async get<T extends typeof StorableComponent>(\n      this: T,\n      identifierDescriptor: IdentifierDescriptor,\n      attributeSelector: AttributeSelector = true,\n      options: {reload?: boolean; throwIfMissing?: boolean; _callerMethodName?: string} = {}\n    ) {\n      identifierDescriptor = this.normalizeIdentifierDescriptor(identifierDescriptor);\n      attributeSelector = normalizeAttributeSelector(attributeSelector);\n\n      const {reload = false, throwIfMissing = true, _callerMethodName} = options;\n\n      let storable = this.getIdentityMap().getComponent(identifierDescriptor) as\n        | InstanceType<T>\n        | undefined;\n\n      const hasPrimaryIdentifier =\n        storable?.getPrimaryIdentifierAttribute().isSet() ||\n        this.prototype.getPrimaryIdentifierAttribute().getName() in identifierDescriptor;\n\n      if (!hasPrimaryIdentifier) {\n        if (this.hasStore()) {\n          // Nothing to do, the storable will be loaded by load()\n        } else if (this.hasRemoteMethod('get')) {\n          // Let's fetch the primary identifier\n          storable = await this.callRemoteMethod(\n            'get',\n            identifierDescriptor,\n            {},\n            {\n              reload,\n              throwIfMissing\n            }\n          );\n\n          if (storable === undefined) {\n            return;\n          }\n        } else {\n          throw new Error(\n            `To be able to execute the get() method${describeCaller(\n              _callerMethodName\n            )} with a secondary identifier, a storable component should be registered in a store or have an exposed get() remote method (${this.describeComponent()})`\n          );\n        }\n      }\n\n      let storableHasBeenCreated = false;\n\n      if (storable === undefined) {\n        storable = this.instantiate(identifierDescriptor);\n        storableHasBeenCreated = true;\n      }\n\n      const loadedStorable = await storable.load(attributeSelector, {\n        reload,\n        throwIfMissing,\n        _callerMethodName: _callerMethodName ?? 'get'\n      });\n\n      if (loadedStorable === undefined && storableHasBeenCreated && storable.isAttached()) {\n        storable.detach();\n      }\n\n      return loadedStorable;\n    }\n\n    /**\n     * Returns whether a storable component instance exists in the store.\n     *\n     * @param identifier A plain object specifying the identifier of the component you want to search. The shape of the object should be `{[identifierName]: identifierValue}`. Alternatively, you can specify a string or a number representing the value of a [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute).\n     * @param [options.reload] A boolean specifying whether a component that has already been loaded should be searched again from the store (default: `false`). Most of the time you will leave this option off to take advantage of the cache.\n     *\n     * @returns A boolean.\n     *\n     * @example\n     * ```\n     * // Check if there is a movie with a certain primary identifier\n     * await Movie.has({id: 'abc123'}); // => true\n     *\n     * // Same as above, but in a short manner\n     * await Movie.has('abc123'); // => true\n     *\n     * // Check if there is a movie with a certain secondary identifier\n     * await Movie.has({slug: 'inception'}); // => true\n     * ```\n     *\n     * @category Storage Operations\n     */\n    static async has(identifierDescriptor: IdentifierDescriptor, options: {reload?: boolean} = {}) {\n      const {reload = false} = options;\n\n      const storable: StorableComponent | undefined = await this.get(\n        identifierDescriptor,\n        {},\n        {reload, throwIfMissing: false, _callerMethodName: 'has'}\n      );\n\n      return storable !== undefined;\n    }\n\n    /**\n     * Loads some attributes of the current storable component instance (and possibly, some of its referenced components) from the store.\n     *\n     * @param [attributeSelector] An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be loaded (default: `true`, which means that all the attributes will be loaded).\n     * @param [options.reload] A boolean specifying whether a component that has already been loaded should be loaded again from the store (default: `false`). Most of the time you will leave this option off to take advantage of the cache.\n     * @param [options.throwIfMissing] A boolean specifying whether an error should be thrown if there is no matching component in the store (default: `true`).\n     *\n     * @returns The current [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storable-component-class) instance.\n     *\n     * @example\n     * ```\n     * // Retrieve a movie with the 'title' attribute only\n     * const movie = await Movie.get('abc123', {title: true});\n     *\n     * // Load a few more movie's attributes\n     * await movie.load({tags: true, rating: true});\n     *\n     * // Load some attributes of the movie's director\n     * await movie.load({director: {fullName: true}});\n     *\n     * // Since the movie's rating has already been loaded,\n     * // it will not be loaded again from the store\n     * await movie.load({rating: true});\n     *\n     * // Change the movie's rating\n     * movie.rating = 8.5;\n     *\n     * // Since the movie's rating has been modified,\n     * // it will be loaded again from the store\n     * await movie.load({rating: true});\n     *\n     * // Force reloading the movie's rating\n     * await movie.load({rating: true}, {reload: true});\n     * ```\n     *\n     * @category Storage Operations\n     */\n    async load<T extends StorableComponent>(\n      this: T,\n      attributeSelector: AttributeSelector | undefined,\n      options: {reload?: boolean; throwIfMissing: false; _callerMethodName?: string}\n    ): Promise<T | undefined>;\n    async load<T extends StorableComponent>(\n      this: T,\n      attributeSelector?: AttributeSelector,\n      options?: {reload?: boolean; throwIfMissing?: boolean; _callerMethodName?: string}\n    ): Promise<T>;\n    @method() async load<T extends StorableComponent>(\n      this: T,\n      attributeSelector: AttributeSelector = true,\n      options: {reload?: boolean; throwIfMissing?: boolean; _callerMethodName?: string} = {}\n    ) {\n      const {reload = false, throwIfMissing = true, _callerMethodName} = options;\n\n      if (this.isNew()) {\n        throw new Error(\n          `Cannot load a storable component that is marked as new (${this.describeComponent()})`\n        );\n      }\n\n      let resolvedAttributeSelector = this.resolveAttributeSelector(attributeSelector);\n\n      if (!reload) {\n        const alreadyLoadedAttributeSelector = this.resolveAttributeSelector(\n          resolvedAttributeSelector,\n          {\n            filter: (attribute: Attribute) =>\n              attribute.getValueSource() === 'server' || attribute.getValueSource() === 'store',\n            setAttributesOnly: true,\n            aggregationMode: 'intersection'\n          }\n        );\n\n        resolvedAttributeSelector = removeFromAttributeSelector(\n          resolvedAttributeSelector,\n          alreadyLoadedAttributeSelector\n        );\n      }\n\n      const computedAttributes = this.getStorableComputedAttributes({\n        attributeSelector: resolvedAttributeSelector\n      });\n\n      let nonComputedAttributeSelector = removeFromAttributeSelector(\n        resolvedAttributeSelector,\n        createAttributeSelectorFromAttributes(computedAttributes)\n      );\n\n      nonComputedAttributeSelector = trimAttributeSelector(nonComputedAttributeSelector);\n\n      let loadedStorable: T | undefined;\n\n      if (nonComputedAttributeSelector !== false) {\n        await this.beforeLoad(nonComputedAttributeSelector);\n\n        const constructor = this.constructor as typeof StorableComponent;\n\n        if (constructor.hasStore()) {\n          loadedStorable = (await constructor.getStore().load(this, {\n            attributeSelector: nonComputedAttributeSelector,\n            throwIfMissing\n          })) as T;\n        } else if (this.hasRemoteMethod('load')) {\n          if (this.getPrimaryIdentifierAttribute().isSet()) {\n            loadedStorable = await this.callRemoteMethod('load', nonComputedAttributeSelector, {\n              reload,\n              throwIfMissing\n            });\n          } else if (this.constructor.hasRemoteMethod('get')) {\n            loadedStorable = await this.constructor.callRemoteMethod(\n              'get',\n              this.getIdentifierDescriptor(),\n              nonComputedAttributeSelector,\n              {\n                reload,\n                throwIfMissing\n              }\n            );\n          } else {\n            throw new Error(\n              `To be able to execute the load() method${describeCaller(\n                _callerMethodName\n              )} when no primary identifier is set, a storable component should be registered in a store or have an exposed get() remote method (${this.constructor.describeComponent()})`\n            );\n          }\n        } else {\n          throw new Error(\n            `To be able to execute the load() method${describeCaller(\n              _callerMethodName\n            )}, a storable component should be registered in a store or have an exposed load() remote method (${this.describeComponent()})`\n          );\n        }\n\n        if (loadedStorable === undefined) {\n          return undefined;\n        }\n\n        await loadedStorable.afterLoad(nonComputedAttributeSelector);\n      } else {\n        loadedStorable = this; // OPTIMIZATION: There was nothing to load\n      }\n\n      for (const attribute of loadedStorable.getStorableAttributesWithLoader({\n        attributeSelector: resolvedAttributeSelector\n      })) {\n        const value = await attribute.callLoader();\n        attribute.setValue(value, {source: 'store'});\n      }\n\n      await loadedStorable.__populate(attributeSelector, {\n        reload,\n        throwIfMissing,\n        _callerMethodName\n      });\n\n      return loadedStorable;\n    }\n\n    async __populate(\n      attributeSelector: AttributeSelector,\n      {\n        reload,\n        throwIfMissing,\n        _callerMethodName\n      }: {reload: boolean; throwIfMissing: boolean; _callerMethodName: string | undefined}\n    ) {\n      const resolvedAttributeSelector = this.resolveAttributeSelector(attributeSelector, {\n        includeReferencedComponents: true\n      });\n\n      const storablesWithAttributeSelectors = new Map<\n        typeof StorableComponent | StorableComponent,\n        AttributeSelector\n      >();\n\n      traverseAttributeSelector(\n        this,\n        resolvedAttributeSelector,\n        (componentOrObject, subattributeSelector) => {\n          if (\n            isStorableClassOrInstance(componentOrObject) &&\n            !ensureStorableClass(componentOrObject).isEmbedded()\n          ) {\n            const storable = componentOrObject;\n\n            if (!storablesWithAttributeSelectors.has(storable)) {\n              storablesWithAttributeSelectors.set(storable, subattributeSelector);\n            } else {\n              const mergedAttributeSelector = mergeAttributeSelectors(\n                storablesWithAttributeSelectors.get(storable)!,\n                subattributeSelector\n              );\n              storablesWithAttributeSelectors.set(storable, mergedAttributeSelector);\n            }\n          }\n        },\n        {includeSubtrees: true, includeLeafs: false}\n      );\n\n      if (storablesWithAttributeSelectors.size > 0) {\n        await Promise.all(\n          Array.from(storablesWithAttributeSelectors).map(\n            ([storable, attributeSelector]) =>\n              isStorableInstance(storable)\n                ? storable.load(attributeSelector, {reload, throwIfMissing, _callerMethodName})\n                : undefined // TODO: Implement class loading\n          )\n        );\n      }\n    }\n\n    /**\n     * Saves the current storable component instance to the store. If the component is new, it will be added to the store with all its attributes. Otherwise, only the attributes that have been modified will be saved to the store.\n     *\n     * @param [attributeSelector] An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be saved (default: `true`, which means that all the modified attributes will be saved).\n     * @param [options.throwIfMissing] A boolean specifying whether an error should be thrown if the current component is not new and there is no existing component with the same identifier in the store (default: `true` if the component is not new).\n     * @param [options.throwIfExists] A boolean specifying whether an error should be thrown if the current component is new and there is an existing component with the same identifier in the store (default: `true` if the component is new).\n     *\n     * @returns The current [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storable-component-class) instance.\n     *\n     * @example\n     * ```\n     * // Retrieve a movie with a few attributes\n     * const movie = await Movie.get('abc123', {title: true, rating: true});\n     *\n     * // Change the movie's rating\n     * movie.rating = 8;\n     *\n     * // Save the new movie's rating to the store\n     * await movie.save();\n     *\n     * // Since the movie's rating has not been changed since the previous save(),\n     * // it will not be saved again\n     * await movie.save();\n     * ```\n     *\n     * @category Storage Operations\n     */\n    async save<T extends StorableComponent>(\n      this: T,\n      attributeSelector: AttributeSelector | undefined,\n      options: {throwIfMissing: false; throwIfExists?: boolean}\n    ): Promise<T | undefined>;\n    async save<T extends StorableComponent>(\n      this: T,\n      attributeSelector: AttributeSelector | undefined,\n      options: {throwIfMissing?: boolean; throwIfExists: false}\n    ): Promise<T | undefined>;\n    async save<T extends StorableComponent>(\n      this: T,\n      attributeSelector?: AttributeSelector,\n      options?: {throwIfMissing?: boolean; throwIfExists?: boolean}\n    ): Promise<T>;\n    @method() async save<T extends StorableComponent>(\n      this: T,\n      attributeSelector: AttributeSelector = true,\n      options: {throwIfMissing?: boolean; throwIfExists?: boolean} = {}\n    ) {\n      const isNew = this.isNew();\n\n      const {throwIfMissing = !isNew, throwIfExists = isNew} = options;\n\n      if (throwIfMissing === true && throwIfExists === true) {\n        throw new Error(\n          \"The 'throwIfMissing' and 'throwIfExists' options cannot be both set to true\"\n        );\n      }\n\n      const computedAttributes = this.getStorableComputedAttributes();\n      const computedAttributeSelector = createAttributeSelectorFromAttributes(computedAttributes);\n\n      let resolvedAttributeSelector = this.resolveAttributeSelector(attributeSelector, {\n        setAttributesOnly: true,\n        target: 'store',\n        aggregationMode: 'intersection'\n      });\n\n      resolvedAttributeSelector = removeFromAttributeSelector(\n        resolvedAttributeSelector,\n        computedAttributeSelector\n      );\n\n      if (!isNew && Object.keys(resolvedAttributeSelector).length < 2) {\n        return this; // OPTIMIZATION: There is nothing to save\n      }\n\n      await this.beforeSave(resolvedAttributeSelector);\n\n      resolvedAttributeSelector = this.resolveAttributeSelector(attributeSelector, {\n        setAttributesOnly: true,\n        target: 'store',\n        aggregationMode: 'intersection'\n      });\n\n      resolvedAttributeSelector = removeFromAttributeSelector(\n        resolvedAttributeSelector,\n        computedAttributeSelector\n      );\n\n      if (!isNew && Object.keys(resolvedAttributeSelector).length < 2) {\n        return this; // OPTIMIZATION: There is nothing to save\n      }\n\n      let savedStorable: T | undefined;\n\n      const constructor = this.constructor as typeof StorableComponent;\n\n      if (constructor.hasStore()) {\n        savedStorable = (await constructor.getStore().save(this, {\n          attributeSelector: resolvedAttributeSelector,\n          throwIfMissing,\n          throwIfExists\n        })) as T;\n      } else if (this.hasRemoteMethod('save')) {\n        savedStorable = await this.callRemoteMethod('save', attributeSelector, {\n          throwIfMissing,\n          throwIfExists\n        });\n      } else {\n        throw new Error(\n          `To be able to execute the save() method, a storable component should be registered in a store or have an exposed save() remote method (${this.describeComponent()})`\n        );\n      }\n\n      if (savedStorable === undefined) {\n        return undefined;\n      }\n\n      await savedStorable.afterSave(resolvedAttributeSelector);\n\n      return savedStorable;\n    }\n\n    _assertArrayItemsAreFullyLoaded(attributeSelector: AttributeSelector) {\n      traverseAttributeSelector(\n        this,\n        attributeSelector,\n        (value, attributeSelector, {isArray}) => {\n          if (isArray && isComponentInstance(value)) {\n            const component = value;\n\n            if (component.constructor.isEmbedded()) {\n              if (\n                !attributeSelectorsAreEqual(\n                  component.resolveAttributeSelector(true),\n                  attributeSelector\n                )\n              ) {\n                throw new Error(\n                  `Cannot save an array item that has some unset attributes (${component.describeComponent()})`\n                );\n              }\n            }\n          }\n        },\n        {includeSubtrees: true, includeLeafs: false}\n      );\n    }\n\n    /**\n     * Deletes the current storable component instance from the store.\n     *\n     * @param [options.throwIfMissing] A boolean specifying whether an error should be thrown if there is no matching component in the store (default: `true`).\n     *\n     * @returns The current [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storable-component-class) instance.\n     *\n     * @example\n     * ```\n     * // Retrieve a movie\n     * const movie = await Movie.get('abc123');\n     *\n     * // Delete the movie\n     * await movie.delete();\n     * ```\n     *\n     * @category Storage Operations\n     */\n    async delete<T extends StorableComponent>(\n      this: T,\n      options: {throwIfMissing: false}\n    ): Promise<T | undefined>;\n    async delete<T extends StorableComponent>(\n      this: T,\n      options?: {throwIfMissing?: boolean}\n    ): Promise<T>;\n    @method() async delete<T extends StorableComponent>(\n      this: T,\n      options: {throwIfMissing?: boolean} = {}\n    ) {\n      if (this.isNew()) {\n        throw new Error(\n          `Cannot delete a storable component that is new (${this.describeComponent()})`\n        );\n      }\n\n      const {throwIfMissing = true} = options;\n\n      const attributeSelector = this.resolveAttributeSelector(true);\n      const computedAttributes = this.getStorableComputedAttributes({attributeSelector});\n      const nonComputedAttributeSelector = removeFromAttributeSelector(\n        attributeSelector,\n        createAttributeSelectorFromAttributes(computedAttributes)\n      );\n\n      await this.beforeDelete(nonComputedAttributeSelector);\n\n      let deletedStorable: T | undefined;\n\n      const constructor = this.constructor as typeof StorableComponent;\n\n      if (constructor.hasStore()) {\n        deletedStorable = (await constructor.getStore().delete(this, {throwIfMissing})) as T;\n      } else if (this.hasRemoteMethod('delete')) {\n        deletedStorable = await this.callRemoteMethod('delete', {throwIfMissing});\n      } else {\n        throw new Error(\n          `To be able to execute the delete() method, a storable component should be registered in a store or have an exposed delete() remote method (${this.describeComponent()})`\n        );\n      }\n\n      if (deletedStorable === undefined) {\n        return undefined;\n      }\n\n      await deletedStorable.afterDelete(nonComputedAttributeSelector);\n\n      deletedStorable.setIsDeletedMark(true);\n\n      // TODO: deletedStorable.detach();\n\n      return deletedStorable;\n    }\n\n    /**\n     * Finds some storable component instances matching the specified query in the store, and load all or some of their attributes (and possibly, load some of their referenced components as well).\n     *\n     * > This method uses the [`load()`](https://layrjs.com/docs/v2/reference/storable#load-instance-method) method under the hood to load the components' attributes. So if you want to expose the [`find()`](https://layrjs.com/docs/v2/reference/storable#find-class-method) method to the frontend, you will typically have to expose the [`load()`](https://layrjs.com/docs/v2/reference/storable#load-instance-method) method as well.\n     *\n     * @param [query] A [`Query`](https://layrjs.com/docs/v2/reference/query) object specifying the criteria to be used when selecting the components from the store (default: `{}`, which means that any component can be selected).\n     * @param [attributeSelector] An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be loaded (default: `true`, which means that all the attributes will be loaded).\n     * @param [options.sort] A plain object specifying how the found components should be sorted (default: `undefined`). The shape of the object should be `{[name]: direction}` where `name` is the name of an attribute, and `direction` is the string `'asc'` or `'desc'` representing the sort direction (ascending or descending).\n     * @param [options.skip] A number specifying how many components should be skipped from the found components (default: `0`).\n     * @param [options.limit] A number specifying the maximum number of components that should be returned (default: `undefined`).\n     * @param [options.reload] A boolean specifying whether a component that has already been loaded should be loaded again from the store (default: `false`). Most of the time you will leave this option off to take advantage of the cache.\n     *\n     * @returns An array of [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storable-component-class) instances.\n     *\n     * @example\n     * ```\n     * // Find all the movies\n     * await Movie.find();\n     *\n     * // Find the Japanese movies\n     * await Movie.find({country: 'Japan'});\n     *\n     * // Find the Japanese drama movies\n     * await Movie.find({country: 'Japan', genre: 'drama'});\n     *\n     * // Find the Tarantino's movies\n     * const tarantino = await Director.get({slug: 'quentin-tarantino'});\n     * await Movie.find({director: tarantino});\n     *\n     * // Find the movies released after 2010\n     * await Movie.find({year: {$greaterThan: 2010}});\n     *\n     * // Find the top 30 movies\n     * await Movie.find({}, true, {sort: {rating: 'desc'}, limit: 30});\n     *\n     * // Find the next top 30 movies\n     * await Movie.find({}, true, {sort: {rating: 'desc'}, skip: 30, limit: 30});\n     * ```\n     *\n     * @category Storage Operations\n     */\n    @method() static async find<T extends typeof StorableComponent>(\n      this: T,\n      query: Query = {},\n      attributeSelector: AttributeSelector = true,\n      options: {sort?: SortDescriptor; skip?: number; limit?: number; reload?: boolean} = {}\n    ) {\n      const {sort, skip, limit, reload = false} = options;\n\n      query = await this.__callStorablePropertyFindersForQuery(query);\n      query = this.__normalizeQuery(query, {loose: !this.hasStore()});\n\n      let foundStorables: InstanceType<T>[];\n\n      if (this.hasStore()) {\n        foundStorables = (await this.getStore().find(this, query, {\n          sort,\n          skip,\n          limit\n        })) as InstanceType<T>[];\n      } else if (this.hasRemoteMethod('find')) {\n        foundStorables = await this.callRemoteMethod('find', query, {}, {sort, skip, limit});\n      } else {\n        throw new Error(\n          `To be able to execute the find() method, a storable component should be registered in a store or have an exposed find() remote method (${this.describeComponent()})`\n        );\n      }\n\n      const loadedStorables = await Promise.all(\n        foundStorables.map((foundStorable) =>\n          foundStorable.load(attributeSelector, {reload, _callerMethodName: 'find'})\n        )\n      );\n\n      return loadedStorables;\n    }\n\n    /**\n     * Counts the number of storable component instances matching the specified query in the store.\n     *\n     * @param [query] A [`Query`](https://layrjs.com/docs/v2/reference/query) object specifying the criteria to be used when selecting the components from the store (default: `{}`, which means that any component can be selected, and therefore the total number of components available in the store will be returned).\n     *\n     * @returns A number.\n     *\n     * @example\n     * ```\n     * // Count the total number of movies\n     * await Movie.count();\n     *\n     * // Count the number of Japanese movies\n     * await Movie.count({country: 'Japan'});\n     *\n     * // Count the number of Japanese drama movies\n     * await Movie.count({country: 'Japan', genre: 'drama'});\n     *\n     * // Count the number of Tarantino's movies\n     * const tarantino = await Director.get({slug: 'quentin-tarantino'})\n     * await Movie.count({director: tarantino});\n     *\n     * // Count the number of movies released after 2010\n     * await Movie.count({year: {$greaterThan: 2010}});\n     * ```\n     *\n     * @category Storage Operations\n     */\n    @method() static async count(query: Query = {}) {\n      query = await this.__callStorablePropertyFindersForQuery(query);\n      query = this.__normalizeQuery(query, {loose: !this.hasStore()});\n\n      let storablesCount: number;\n\n      if (this.hasStore()) {\n        storablesCount = await this.getStore().count(this, query);\n      } else if (this.hasRemoteMethod('count')) {\n        storablesCount = await this.callRemoteMethod('count', query);\n      } else {\n        throw new Error(\n          `To be able to execute the count() method, a storable component should be registered in a store or have an exposed count() remote method (${this.describeComponent()})`\n        );\n      }\n\n      return storablesCount;\n    }\n\n    static async __callStorablePropertyFindersForQuery(query: Query) {\n      for (const property of this.prototype.getStorablePropertiesWithFinder()) {\n        const name = property.getName();\n\n        if (!hasOwnProperty(query, name)) {\n          continue; // The property finder is not used in the query\n        }\n\n        const {[name]: value, ...remainingQuery} = query;\n\n        const finderQuery = await property.callFinder(value);\n\n        query = {...remainingQuery, ...finderQuery};\n      }\n\n      return query;\n    }\n\n    static __normalizeQuery(query: Query, {loose = false}: {loose?: boolean} = {}) {\n      const normalizeQueryForComponent = function (\n        query: Query | typeof Component | Component,\n        component: typeof Component | Component\n      ) {\n        if (isComponentClassOrInstance(query)) {\n          if (component === query || isPrototypeOf(component, query)) {\n            return query.toObject({minimize: true});\n          }\n\n          throw new Error(\n            `An unexpected component was specified in a query (${component.describeComponent({\n              componentPrefix: 'expected'\n            })}, ${query.describeComponent({componentPrefix: 'specified'})})`\n          );\n        }\n\n        if (!isPlainObject(query)) {\n          throw new Error(\n            `Expected a plain object in a query, but received a value of type '${getTypeOf(query)}'`\n          );\n        }\n\n        const normalizedQuery: Query = {};\n\n        for (const [name, subquery] of Object.entries<Query>(query)) {\n          if (name === '$some' || name === '$every') {\n            normalizedQuery[name] = normalizeQueryForComponent(subquery, component);\n            continue;\n          }\n\n          if (name === '$length') {\n            normalizedQuery[name] = subquery;\n            continue;\n          }\n\n          if (name === '$not') {\n            normalizedQuery[name] = normalizeQueryForComponent(subquery, component);\n            continue;\n          }\n\n          if (name === '$and' || name === '$or' || name === '$nor') {\n            if (!Array.isArray(subquery)) {\n              throw new Error(\n                `Expected an array as value of the operator '${name}', but received a value of type '${getTypeOf(\n                  subquery\n                )}'`\n              );\n            }\n\n            const subqueries: Query[] = subquery;\n            normalizedQuery[name] = subqueries.map((subquery) =>\n              normalizeQueryForComponent(subquery, component)\n            );\n            continue;\n          }\n\n          if (name === '$in') {\n            if (!Array.isArray(subquery)) {\n              throw new Error(\n                `Expected an array as value of the operator '${name}', but received a value of type '${getTypeOf(\n                  subquery\n                )}'`\n              );\n            }\n\n            if (!isComponentInstance(component)) {\n              throw new Error(\n                `The operator '${name}' cannot be used in the context of a component class`\n              );\n            }\n\n            const nestedComponents: Component[] = subquery;\n\n            const primaryIdentifiers: IdentifierValue[] = nestedComponents.map(\n              (nestedComponent) => {\n                if (!isComponentInstance(nestedComponent)) {\n                  throw new Error(\n                    `Expected an array of component instances as value of the operator '${name}', but received a value of type '${getTypeOf(\n                      nestedComponent\n                    )}'`\n                  );\n                }\n\n                if (!isPrototypeOf(component, nestedComponent)) {\n                  throw new Error(\n                    `An unexpected item was specified for the operator '${name}' (${component.describeComponent(\n                      {\n                        componentPrefix: 'expected'\n                      }\n                    )}, ${nestedComponent.describeComponent({componentPrefix: 'specified'})})`\n                  );\n                }\n\n                return nestedComponent.getPrimaryIdentifierAttribute().getValue()!;\n              }\n            );\n\n            const primaryIdentifierAttributeName = component\n              .getPrimaryIdentifierAttribute()\n              .getName();\n\n            normalizedQuery[primaryIdentifierAttributeName] = {[name]: primaryIdentifiers};\n            continue;\n          }\n\n          if (component.hasAttribute(name)) {\n            const attribute = component.getAttribute(name);\n\n            normalizedQuery[name] = normalizeQueryForAttribute(subquery, attribute);\n          } else {\n            if (!loose) {\n              throw new Error(\n                `An unknown attribute was specified in a query (${component.describeComponent()}, attribute: '${name}')`\n              );\n            }\n\n            normalizedQuery[name] = subquery;\n          }\n        }\n\n        return normalizedQuery;\n      };\n\n      const normalizeQueryForAttribute = function (query: Query, attribute: Attribute) {\n        const type = attribute.getValueType();\n\n        return normalizeQueryForAttributeAndType(query, attribute, type);\n      };\n\n      const normalizeQueryForAttributeAndType = function (\n        query: Query,\n        attribute: Attribute,\n        type: ValueType\n      ): Query {\n        if (isComponentValueTypeInstance(type)) {\n          const component = type.getComponent(attribute);\n\n          const normalizedQuery = normalizeQueryForComponent(query, component);\n\n          return normalizedQuery;\n        }\n\n        if (isArrayValueTypeInstance(type)) {\n          const itemType = type.getItemType();\n\n          let normalizedQuery = normalizeQueryForAttributeAndType(query, attribute, itemType);\n\n          if (isPlainObject(normalizedQuery) && '$includes' in normalizedQuery) {\n            // Make '$includes' an alias of '$some'\n            normalizedQuery = mapKeys(normalizedQuery, (_value, key) =>\n              key === '$includes' ? '$some' : key\n            );\n          }\n\n          if (\n            !(\n              isPlainObject(normalizedQuery) &&\n              ('$some' in normalizedQuery ||\n                '$every' in normalizedQuery ||\n                '$length' in normalizedQuery ||\n                '$not' in normalizedQuery)\n            )\n          ) {\n            // Make '$some' implicit\n            normalizedQuery = {$some: normalizedQuery};\n          }\n\n          return normalizedQuery;\n        }\n\n        return query;\n      };\n\n      return normalizeQueryForComponent(query, this.prototype);\n    }\n\n    // === isDeleted Mark ===\n\n    __isDeleted: boolean | undefined;\n\n    /**\n     * Returns whether the component instance is marked as deleted or not.\n     *\n     * @returns A boolean.\n     *\n     * @example\n     * ```\n     * movie.getIsDeletedMark(); // => false\n     * await movie.delete();\n     * movie.getIsDeletedMark(); // => true\n     * ```\n     *\n     * @category isDeleted Mark\n     */\n    getIsDeletedMark() {\n      return this.__isDeleted === true;\n    }\n\n    /**\n     * Sets whether the component instance is marked as deleted or not.\n     *\n     * @param isDeleted A boolean specifying if the component instance should be marked as deleted or not.\n     *\n     * @example\n     * ```\n     * movie.getIsDeletedMark(); // => false\n     * movie.setIsDeletedMark(true);\n     * movie.getIsDeletedMark(); // => true\n     * ```\n     *\n     * @category isDeleted Mark\n     */\n    setIsDeletedMark(isDeleted: boolean) {\n      Object.defineProperty(this, '__isDeleted', {value: isDeleted, configurable: true});\n    }\n\n    // === Hooks ===\n\n    /**\n     * A method that you can override to execute some custom logic just before the current storable component instance is loaded from the store.\n     *\n     * This method is automatically called when the [`load()`](https://layrjs.com/docs/v2/reference/storable#load-instance-method), [`get()`](https://layrjs.com/docs/v2/reference/storable#get-class-method), or [`find()`](https://layrjs.com/docs/v2/reference/storable#find-class-method) method is called, and there are some attributes to load. If all the attributes have already been loaded by a previous operation, unless the `reload` option is used, this method is not called.\n     *\n     * @param attributeSelector An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) indicating the attributes that will be loaded.\n     *\n     * @example\n     * ```\n     * // JS\n     *\n     * class Movie extends Storable(Component) {\n     *   // ...\n     *\n     *   async beforeLoad(attributeSelector) {\n     *     // Don't forget to call the parent method\n     *     await super.beforeLoad(attributeSelector);\n     *\n     *     // Implement your custom logic here\n     *   }\n     * }\n     * ```\n     *\n     * @example\n     * ```\n     * // TS\n     *\n     * class Movie extends Storable(Component) {\n     *   // ...\n     *\n     *   async beforeLoad(attributeSelector: AttributeSelector) {\n     *     // Don't forget to call the parent method\n     *     await super.beforeLoad(attributeSelector);\n     *\n     *     // Implement your custom logic here\n     *   }\n     * }\n     * ```\n     *\n     * @category Hooks\n     */\n    async beforeLoad(attributeSelector: AttributeSelector) {\n      await this.__callStorableAttributeHooks('beforeLoad', {attributeSelector});\n    }\n\n    /**\n     * A method that you can override to execute some custom logic just after the current storable component instance has been loaded from the store.\n     *\n     * This method is automatically called when the [`load()`](https://layrjs.com/docs/v2/reference/storable#load-instance-method), [`get()`](https://layrjs.com/docs/v2/reference/storable#get-class-method), or [`find()`](https://layrjs.com/docs/v2/reference/storable#find-class-method) method is called, and there were some attributes to load. If all the attributes have already been loaded by a previous operation, unless the `reload` option is used, this method is not called.\n     *\n     * @param attributeSelector An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) indicating the attributes that were loaded.\n     *\n     * @example\n     * ```\n     * // JS\n     *\n     * class Movie extends Storable(Component) {\n     *   // ...\n     *\n     *   async afterLoad(attributeSelector) {\n     *     // Don't forget to call the parent method\n     *     await super.afterLoad(attributeSelector);\n     *\n     *     // Implement your custom logic here\n     *   }\n     * }\n     * ```\n     *\n     * @example\n     * ```\n     * // TS\n     *\n     * class Movie extends Storable(Component) {\n     *   // ...\n     *\n     *   async afterLoad(attributeSelector: AttributeSelector) {\n     *     // Don't forget to call the parent method\n     *     await super.afterLoad(attributeSelector);\n     *\n     *     // Implement your custom logic here\n     *   }\n     * }\n     * ```\n     *\n     * @category Hooks\n     */\n    async afterLoad(attributeSelector: AttributeSelector) {\n      await this.__callStorableAttributeHooks('afterLoad', {\n        attributeSelector,\n        setAttributesOnly: true\n      });\n    }\n\n    /**\n     * A method that you can override to execute some custom logic just before the current storable component instance is saved to the store.\n     *\n     * This method is automatically called when the [`save()`](https://layrjs.com/docs/v2/reference/storable#save-instance-method) method is called, and there are some modified attributes to save. If no attributes were modified, this method is not called.\n     *\n     * @param attributeSelector An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) indicating the attributes that will be saved.\n     *\n     * @example\n     * ```\n     * // JS\n     *\n     * class Movie extends Storable(Component) {\n     *   // ...\n     *\n     *   async beforeSave(attributeSelector) {\n     *     // Don't forget to call the parent method\n     *     await super.beforeSave(attributeSelector);\n     *\n     *     // Implement your custom logic here\n     *   }\n     * }\n     * ```\n     *\n     * @example\n     * ```\n     * // TS\n     *\n     * class Movie extends Storable(Component) {\n     *   // ...\n     *\n     *   async beforeSave(attributeSelector: AttributeSelector) {\n     *     // Don't forget to call the parent method\n     *     await super.beforeSave(attributeSelector);\n     *\n     *     // Implement your custom logic here\n     *   }\n     * }\n     * ```\n     *\n     * @category Hooks\n     */\n    async beforeSave(attributeSelector: AttributeSelector) {\n      await this.__callStorableAttributeHooks('beforeSave', {\n        attributeSelector,\n        setAttributesOnly: true\n      });\n    }\n\n    /**\n     * A method that you can override to execute some custom logic just after the current storable component instance has been saved to the store.\n     *\n     * This method is automatically called when the [`save()`](https://layrjs.com/docs/v2/reference/storable#save-instance-method) method is called, and there were some modified attributes to save. If no attributes were modified, this method is not called.\n     *\n     * @param attributeSelector An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) indicating the attributes that were saved.\n     *\n     * @example\n     * ```\n     * // JS\n     *\n     * class Movie extends Storable(Component) {\n     *   // ...\n     *\n     *   async afterSave(attributeSelector) {\n     *     // Don't forget to call the parent method\n     *     await super.afterSave(attributeSelector);\n     *\n     *     // Implement your custom logic here\n     *   }\n     * }\n     * ```\n     *\n     * @example\n     * ```\n     * // TS\n     *\n     * class Movie extends Storable(Component) {\n     *   // ...\n     *\n     *   async afterSave(attributeSelector: AttributeSelector) {\n     *     // Don't forget to call the parent method\n     *     await super.afterSave(attributeSelector);\n     *\n     *     // Implement your custom logic here\n     *   }\n     * }\n     * ```\n     *\n     * @category Hooks\n     */\n    async afterSave(attributeSelector: AttributeSelector) {\n      await this.__callStorableAttributeHooks('afterSave', {\n        attributeSelector,\n        setAttributesOnly: true\n      });\n    }\n\n    /**\n     * A method that you can override to execute some custom logic just before the current storable component instance is deleted from the store.\n     *\n     * This method is automatically called when the [`delete()`](https://layrjs.com/docs/v2/reference/storable#delete-instance-method) method is called.\n     *\n     * @param attributeSelector An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) indicating the attributes that will be deleted.\n     *\n     * @example\n     * ```\n     * // JS\n     *\n     * class Movie extends Storable(Component) {\n     *   // ...\n     *\n     *   async beforeDelete(attributeSelector) {\n     *     // Don't forget to call the parent method\n     *     await super.beforeDelete(attributeSelector);\n     *\n     *     // Implement your custom logic here\n     *   }\n     * }\n     * ```\n     *\n     * @example\n     * ```\n     * // TS\n     *\n     * class Movie extends Storable(Component) {\n     *   // ...\n     *\n     *   async beforeDelete(attributeSelector: AttributeSelector) {\n     *     // Don't forget to call the parent method\n     *     await super.beforeDelete(attributeSelector);\n     *\n     *     // Implement your custom logic here\n     *   }\n     * }\n     * ```\n     *\n     * @category Hooks\n     */\n    async beforeDelete(attributeSelector: AttributeSelector) {\n      await this.__callStorableAttributeHooks('beforeDelete', {\n        attributeSelector,\n        setAttributesOnly: true\n      });\n    }\n\n    /**\n     * A method that you can override to execute some custom logic just after the current storable component instance has been deleted from the store.\n     *\n     * This method is automatically called when the [`delete()`](https://layrjs.com/docs/v2/reference/storable#delete-instance-method) method is called.\n     *\n     * @param attributeSelector An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) indicating the attributes that were deleted.\n     *\n     * @example\n     * ```\n     * // JS\n     *\n     * class Movie extends Storable(Component) {\n     *   // ...\n     *\n     *   async afterDelete(attributeSelector) {\n     *     // Don't forget to call the parent method\n     *     await super.afterDelete(attributeSelector);\n     *\n     *     // Implement your custom logic here\n     *   }\n     * }\n     * ```\n     *\n     * @example\n     * ```\n     * // TS\n     *\n     * class Movie extends Storable(Component) {\n     *   // ...\n     *\n     *   async afterDelete(attributeSelector: AttributeSelector) {\n     *     // Don't forget to call the parent method\n     *     await super.afterDelete(attributeSelector);\n     *\n     *     // Implement your custom logic here\n     *   }\n     * }\n     * ```\n     *\n     * @category Hooks\n     */\n    async afterDelete(attributeSelector: AttributeSelector) {\n      await this.__callStorableAttributeHooks('afterDelete', {\n        attributeSelector,\n        setAttributesOnly: true\n      });\n    }\n\n    // === Observability ===\n\n    /**\n     * See the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) class.\n     *\n     * @category Observability\n     */\n\n    // === Utilities ===\n\n    static get isStorable() {\n      return this.prototype.isStorable;\n    }\n\n    isStorable(value: any): value is typeof StorableComponent | StorableComponent {\n      return isStorable(value);\n    }\n  }\n\n  Object.defineProperty(Storable, '__mixin', {value: 'Storable'});\n\n  return Storable;\n}\n\n// Make sure the name of the Storable mixin persists over minification\nObject.defineProperty(Storable, 'displayName', {value: 'Storable'});\n\nexport class StorableComponent extends Storable(Component) {}\n\nfunction describeCaller(callerMethodName: string | undefined) {\n  return callerMethodName !== undefined ? ` (called from ${callerMethodName}())` : '';\n}\n"
  },
  {
    "path": "packages/storable/src/store-like.ts",
    "content": "import {AttributeSelector} from '@layr/component';\n\nimport type {StorableComponent} from './storable';\nimport type {Query} from './query';\nimport type {SortDescriptor} from './storable';\n\nexport declare class StoreLike {\n  getURL: () => string | undefined;\n\n  load: (\n    storable: StorableComponent,\n    options?: {attributeSelector?: AttributeSelector; throwIfMissing?: boolean}\n  ) => Promise<StorableComponent | undefined>;\n\n  save: (\n    storable: StorableComponent,\n    options?: {\n      attributeSelector?: AttributeSelector;\n      throwIfMissing?: boolean;\n      throwIfExists?: boolean;\n    }\n  ) => Promise<StorableComponent | undefined>;\n\n  delete: (\n    storable: StorableComponent,\n    options?: {throwIfMissing?: boolean}\n  ) => Promise<StorableComponent | undefined>;\n\n  find: (\n    storable: typeof StorableComponent,\n    query?: Query,\n    options?: {sort?: SortDescriptor; skip?: number; limit?: number}\n  ) => Promise<StorableComponent[]>;\n\n  count: (storable: typeof StorableComponent, query?: Query) => Promise<number>;\n}\n"
  },
  {
    "path": "packages/storable/src/utilities.ts",
    "content": "import {getTypeOf} from 'core-helpers';\n\nimport type {StorableComponent} from './storable';\n\n/**\n * Returns whether the specified value is a storable component class.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isStorableClass(value: any): value is typeof StorableComponent {\n  return typeof value === 'function' && typeof value.isStorable === 'function';\n}\n\n/**\n * Returns whether the specified value is a storable component instance.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isStorableInstance(value: any): value is StorableComponent {\n  return typeof value === 'object' && value !== null && typeof value.isStorable === 'function';\n}\n\n/**\n * Returns whether the specified value is a storable component class or instance.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isStorableClassOrInstance(\n  value: any\n): value is typeof StorableComponent | StorableComponent {\n  return isStorable(value);\n}\n\nexport function isStorable(value: any): value is typeof StorableComponent | StorableComponent {\n  return typeof value?.isStorable === 'function';\n}\n\n/**\n * Throws an error if the specified value is not a storable component class.\n *\n * @param value A value of any type.\n *\n * @category Utilities\n */\nexport function assertIsStorableClass(value: any): asserts value is typeof StorableComponent {\n  if (!isStorableClass(value)) {\n    throw new Error(\n      `Expected a storable component class, but received a value of type '${getTypeOf(value)}'`\n    );\n  }\n}\n\n/**\n * Throws an error if the specified value is not a storable component instance.\n *\n * @param value A value of any type.\n *\n * @category Utilities\n */\nexport function assertIsStorableInstance(value: any): asserts value is StorableComponent {\n  if (!isStorableInstance(value)) {\n    throw new Error(\n      `Expected a storable component instance, but received a value of type '${getTypeOf(value)}'`\n    );\n  }\n}\n\n/**\n * Throws an error if the specified value is not a storable component class or instance.\n *\n * @param value A value of any type.\n *\n * @category Utilities\n */\nexport function assertIsStorableClassOrInstance(\n  value: any\n): asserts value is typeof StorableComponent | StorableComponent {\n  if (!isStorableClassOrInstance(value)) {\n    throw new Error(\n      `Expected a storable component class or instance, but received a value of type '${getTypeOf(\n        value\n      )}'`\n    );\n  }\n}\n\n/**\n * Ensures that the specified storable component is a class. If you specify a storable component instance (or prototype), the class of the component is returned. If you specify a storable component class, it is returned as is.\n *\n * @param component A storable component class or instance.\n *\n * @returns A storable component class.\n *\n * @example\n * ```\n * ensureStorableClass(movie) => Movie\n * ensureStorableClass(Movie.prototype) => Movie\n * ensureStorableClass(Movie) => Movie\n * ```\n *\n * @category Utilities\n */\nexport function ensureStorableClass(storable: typeof StorableComponent | StorableComponent) {\n  if (isStorableClass(storable)) {\n    return storable;\n  }\n\n  if (isStorableInstance(storable)) {\n    return storable.constructor as typeof StorableComponent;\n  }\n\n  throw new Error(\n    `Expected a storable component class or instance, but received a value of type '${getTypeOf(\n      storable\n    )}'`\n  );\n}\n\n/**\n * Ensures that the specified storable component is an instance (or prototype). If you specify a storable component class, the component prototype is returned. If you specify a storable component instance (or prototype), it is returned as is.\n *\n * @param component A storable component class or instance.\n *\n * @returns A storable component instance (or prototype).\n *\n * @example\n * ```\n * ensureStorableInstance(Movie) => Movie.prototype\n * ensureStorableInstance(Movie.prototype) => Movie.prototype\n * ensureStorableInstance(movie) => movie\n * ```\n *\n * @category Utilities\n */\nexport function ensureStorableInstance(component: typeof StorableComponent | StorableComponent) {\n  if (isStorableClass(component)) {\n    return component.prototype;\n  }\n\n  if (isStorableInstance(component)) {\n    return component;\n  }\n\n  throw new Error(\n    `Expected a storable component class or instance, but received a value of type '${getTypeOf(\n      component\n    )}'`\n  );\n}\n"
  },
  {
    "path": "packages/storable/tsconfig.json",
    "content": "{\n  \"extends\": \"@mvila/tsconfig\",\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/store/README.md",
    "content": "# @layr/store\n\nA base class for implementing Layr stores.\n\n## Installation\n\n```\nnpm install @layr/store\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/store/package.json",
    "content": "{\n  \"name\": \"@layr/store\",\n  \"version\": \"2.0.81\",\n  \"description\": \"A base class for implementing Layr stores\",\n  \"keywords\": [\n    \"layr\",\n    \"store\",\n    \"persistence\",\n    \"storage\",\n    \"database\"\n  ],\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/layrjs/layr/tree/master/packages/store\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"dist/node-cjs/index.js\",\n  \"module\": \"dist/node-esm/index.js\",\n  \"engines\": {\n    \"node\": \">=16.0.0\"\n  },\n  \"scripts\": {\n    \"build\": \"dev-tools build:ts-library\",\n    \"prepare\": \"npm run build\",\n    \"test\": \"dev-tools test:ts-library\",\n    \"publish:package\": \"dev-tools publish:package\",\n    \"update\": \"dev-tools update:dependencies\"\n  },\n  \"dependencies\": {\n    \"@layr/component\": \"^2.0.51\",\n    \"@layr/storable\": \"^2.0.76\",\n    \"core-helpers\": \"^1.0.8\",\n    \"lodash\": \"^4.17.21\",\n    \"simple-serialization\": \"^1.0.12\",\n    \"tslib\": \"^2.4.1\"\n  },\n  \"devDependencies\": {\n    \"@mvila/dev-tools\": \"^1.3.1\",\n    \"@mvila/tsconfig\": \"^1.0.6\",\n    \"@types/jest\": \"^29.2.5\",\n    \"@types/lodash\": \"^4.14.191\"\n  }\n}\n"
  },
  {
    "path": "packages/store/src/document.ts",
    "content": "import {AttributeSelector, iterateOverAttributeSelector} from '@layr/component';\nimport {PlainObject, isPlainObject, deleteUndefinedProperties} from 'core-helpers';\n\nimport type {Path} from './path';\n\nexport type Document = PlainObject;\n\nexport type AttributeValue = undefined | null | boolean | number | string | Date;\n\nexport type Projection = {[path: string]: 1};\n\nexport type DocumentPatch = {$set?: {[path: string]: any}; $unset?: {[path: string]: 1}};\n\n// {a: true, b: {c: true}} => {__component: 1, a: 1, \"b.__component\": 1, \"b.c\": 1}\nexport function buildProjection(attributeSelector: AttributeSelector) {\n  if (typeof attributeSelector === 'boolean') {\n    if (attributeSelector === true) {\n      return undefined;\n    }\n\n    return {__component: 1} as Projection; // Always include the '__component' attribute\n  }\n\n  const projection: Projection = {};\n\n  const build = function (attributeSelector: Exclude<AttributeSelector, boolean>, path: Path) {\n    attributeSelector = {__component: true, ...attributeSelector};\n\n    for (const [name, subattributeSelector] of iterateOverAttributeSelector(attributeSelector)) {\n      const subpath: Path = (path !== '' ? path + '.' : '') + name;\n\n      if (typeof subattributeSelector === 'boolean') {\n        if (subattributeSelector === true) {\n          projection[subpath] = 1;\n        }\n      } else {\n        build(subattributeSelector, subpath);\n      }\n    }\n  };\n\n  build(attributeSelector, '');\n\n  return projection;\n}\n\nexport function buildDocumentPatch(document: Document) {\n  const documentPatch: DocumentPatch = {};\n\n  const build = function (document: unknown, path: Path) {\n    if (document === undefined) {\n      if (documentPatch.$unset === undefined) {\n        documentPatch.$unset = {};\n      }\n\n      documentPatch.$unset[path] = 1;\n\n      return;\n    }\n\n    if (isPlainObject(document) && '__component' in document) {\n      for (const [name, value] of Object.entries(document)) {\n        const subpath: Path = (path !== '' ? path + '.' : '') + name;\n        build(value, subpath);\n      }\n\n      return;\n    }\n\n    if (isPlainObject(document) || Array.isArray(document)) {\n      deleteUndefinedProperties(document);\n    }\n\n    if (documentPatch.$set === undefined) {\n      documentPatch.$set = {};\n    }\n\n    documentPatch.$set[path] = document;\n  };\n\n  build(document, '');\n\n  return documentPatch;\n}\n"
  },
  {
    "path": "packages/store/src/expression.ts",
    "content": "import type {Operator} from '@layr/storable';\n\nimport type {AttributeValue} from './document';\nimport type {Path} from './path';\n\nexport type Expression = [Path, Operator, Operand];\n\nexport type Operand = AttributeValue | Expression[] | Expression[][];\n"
  },
  {
    "path": "packages/store/src/index.ts",
    "content": "export * from './document';\nexport * from './expression';\nexport * from './path';\nexport * from './store';\nexport * from './utilities';\n"
  },
  {
    "path": "packages/store/src/path.ts",
    "content": "export type Path = string;\n"
  },
  {
    "path": "packages/store/src/store.test.ts",
    "content": "import {Component, EmbeddedComponent, provide} from '@layr/component';\nimport {Storable, primaryIdentifier, secondaryIdentifier, attribute, index} from '@layr/storable';\n\nimport {Store} from './store';\nimport {isStoreInstance} from './utilities';\n\ndescribe('Store', () => {\n  class MockStore extends Store {\n    async createDocument() {\n      return false;\n    }\n\n    async readDocument() {\n      return undefined;\n    }\n\n    async updateDocument() {\n      return false;\n    }\n\n    async deleteDocument() {\n      return false;\n    }\n\n    async findDocuments() {\n      return [];\n    }\n\n    async countDocuments() {\n      return 0;\n    }\n\n    async migrateCollection() {\n      return {\n        name: 'Component',\n        createdIndexes: [],\n        droppedIndexes: []\n      };\n    }\n  }\n\n  test('Creation', async () => {\n    const store = new MockStore();\n\n    expect(isStoreInstance(store)).toBe(true);\n\n    expect(() => new MockStore({unknown: true})).toThrow(\n      \"Did not expect the option 'unknown' to exist\"\n    );\n  });\n\n  test('registerRootComponent() and getRootComponents()', async () => {\n    class Profile extends Storable(Component) {}\n\n    class User extends Storable(Component) {\n      @provide() static Profile = Profile;\n    }\n\n    class MovieDetails extends Storable(EmbeddedComponent) {}\n\n    class Movie extends Storable(Component) {\n      @provide() static MovieDetails = MovieDetails;\n    }\n\n    class Root extends Component {\n      @provide() static User = User;\n      @provide() static Movie = Movie;\n    }\n\n    const store = new MockStore();\n\n    store.registerRootComponent(Root);\n\n    expect(Array.from(store.getRootComponents())).toEqual([Root]);\n\n    expect(Array.from(store.getStorables())).toEqual([User, Profile, Movie]);\n  });\n\n  test('getStorable() and hasStorable()', async () => {\n    class User extends Storable(Component) {}\n\n    const store = new MockStore();\n\n    expect(store.hasStorable('User')).toBe(false);\n    expect(() => store.getStorable('User')).toThrow(\n      \"The storable component 'User' is not registered in the store\"\n    );\n\n    store.registerRootComponent(User);\n\n    expect(store.hasStorable('User')).toBe(true);\n    expect(store.getStorable('User')).toBe(User);\n  });\n\n  test('getStorableOfType()', async () => {\n    class User extends Storable(Component) {}\n\n    const store = new MockStore();\n\n    store.registerRootComponent(User);\n\n    expect(store.getStorableOfType('typeof User')).toBe(User);\n    expect(store.getStorableOfType('User')).toBe(User.prototype);\n\n    expect(() => store.getStorableOfType('typeof Movie')).toThrow(\n      \"The storable component of type 'typeof Movie' is not registered in the store\"\n    );\n  });\n\n  test('registerStorable()', async () => {\n    class User extends Storable(Component) {}\n\n    const store = new MockStore();\n\n    store.registerStorable(User);\n\n    expect(store.getStorable('User')).toBe(User);\n\n    // Registering a storable twice in the same store should be okay\n    store.registerStorable(User);\n\n    expect(store.getStorable('User')).toBe(User);\n\n    class UserDetails extends Storable(EmbeddedComponent) {}\n\n    expect(() => store.registerStorable(UserDetails)).toThrow(\n      \"Cannot register an embedded storable component (component: 'UserDetails')\"\n    );\n\n    class NotAStorable {}\n\n    // @ts-expect-error\n    expect(() => store.registerStorable(NotAStorable)).toThrow(\n      \"Expected a storable component class, but received a value of type 'typeof NotAStorable'\"\n    );\n\n    const store2 = new MockStore();\n\n    expect(() => store2.registerStorable(User)).toThrow(\n      \"Cannot register a storable component that is already registered in another store (component: 'User')\"\n    );\n\n    class User2 extends Storable(Component) {}\n\n    User2.setComponentName('User');\n\n    expect(() => store.registerStorable(User2)).toThrow(\n      \"A storable component with the same name is already registered (component: 'User')\"\n    );\n  });\n\n  test('getStorables()', async () => {\n    class User extends Storable(Component) {}\n\n    class Movie extends Storable(Component) {}\n\n    const store = new MockStore();\n\n    expect(Array.from(store.getStorables())).toEqual([]);\n\n    store.registerStorable(User);\n\n    expect(Array.from(store.getStorables())).toEqual([User]);\n\n    store.registerStorable(Movie);\n\n    expect(Array.from(store.getStorables())).toEqual([User, Movie]);\n  });\n\n  test('getCollectionSchema()', async () => {\n    class Person extends Storable(Component) {\n      @primaryIdentifier() id!: string;\n\n      @attribute('string') fullName!: string;\n    }\n\n    class MovieDetails extends Storable(EmbeddedComponent) {\n      @index() @attribute('number') duration!: number;\n\n      @attribute('string') aspectRatio!: string;\n    }\n\n    @index({year: 'desc', title: 'asc'})\n    @index({director: 'asc', title: 'asc'})\n    class Movie extends Storable(Component) {\n      @provide() static Person = Person;\n\n      @provide() static MovieDetails = MovieDetails;\n\n      @primaryIdentifier() id!: string;\n\n      @secondaryIdentifier() slug!: string;\n\n      @index({isUnique: true}) @attribute('string') title!: string;\n\n      @index({direction: 'desc'}) @attribute('number') year!: number;\n\n      @attribute('string') summary!: string;\n\n      @index() @attribute('string[]') genres!: string[];\n\n      @attribute('Person') director!: Person;\n\n      @attribute('Person[]') actors!: Person[];\n\n      @attribute('MovieDetails') details!: MovieDetails;\n    }\n\n    const store = new MockStore();\n\n    store.registerStorable(Movie);\n\n    const schema = store.getCollectionSchema(Movie.prototype);\n\n    expect(schema).toStrictEqual({\n      indexes: [\n        {attributes: {id: 'asc'}, isPrimary: true, isUnique: true},\n        {attributes: {slug: 'asc'}, isPrimary: false, isUnique: true},\n        {\n          attributes: {'director.id': 'asc'},\n          isPrimary: false,\n          isUnique: false\n        },\n        {\n          attributes: {'actors.id': 'asc'},\n          isPrimary: false,\n          isUnique: false\n        },\n        {attributes: {'details.duration': 'asc'}, isPrimary: false, isUnique: false},\n        {attributes: {title: 'asc'}, isPrimary: false, isUnique: true},\n        {attributes: {year: 'desc'}, isPrimary: false, isUnique: false},\n        {attributes: {genres: 'asc'}, isPrimary: false, isUnique: false},\n        {\n          attributes: {'director.id': 'asc', 'title': 'asc'},\n          isPrimary: false,\n          isUnique: false\n        },\n        {\n          attributes: {year: 'desc', title: 'asc'},\n          isPrimary: false,\n          isUnique: false\n        }\n      ]\n    });\n  });\n});\n"
  },
  {
    "path": "packages/store/src/store.ts",
    "content": "import {\n  Component,\n  deserialize,\n  isComponentInstance,\n  ensureComponentClass,\n  assertIsComponentClass,\n  assertIsComponentType,\n  getComponentNameFromComponentClassType,\n  getComponentNameFromComponentInstanceType,\n  NormalizedIdentifierDescriptor,\n  AttributeSelector,\n  createAttributeSelectorFromNames,\n  pickFromAttributeSelector,\n  mergeAttributeSelectors,\n  isIdentifierAttributeInstance,\n  isPrimaryIdentifierAttributeInstance,\n  isComponentValueTypeInstance\n} from '@layr/component';\nimport {\n  StorableComponent,\n  isStorableClass,\n  isStorableInstance,\n  assertIsStorableClass,\n  Query,\n  SortDescriptor,\n  SortDirection,\n  Operator,\n  looksLikeOperator,\n  normalizeOperatorForValue\n} from '@layr/storable';\nimport {\n  isPlainObject,\n  deleteUndefinedProperties,\n  assertNoUnknownOptions,\n  PromiseLikeValue\n} from 'core-helpers';\nimport {serialize as simpleSerialize, deserialize as simpleDeserialize} from 'simple-serialization';\nimport mapKeys from 'lodash/mapKeys';\n\nimport {\n  Document,\n  AttributeValue,\n  Projection,\n  buildProjection,\n  DocumentPatch,\n  buildDocumentPatch\n} from './document';\nimport type {Expression} from './expression';\nimport type {Path} from './path';\nimport {isStoreInstance} from './utilities';\n\nexport type CreateDocumentParams = {\n  collectionName: string;\n  identifierDescriptor: NormalizedIdentifierDescriptor;\n  document: Document;\n};\n\nexport type ReadDocumentParams = {\n  collectionName: string;\n  identifierDescriptor: NormalizedIdentifierDescriptor;\n  projection?: Projection;\n};\n\nexport type UpdateDocumentParams = {\n  collectionName: string;\n  identifierDescriptor: NormalizedIdentifierDescriptor;\n  documentPatch: DocumentPatch;\n};\n\nexport type DeleteDocumentParams = {\n  collectionName: string;\n  identifierDescriptor: NormalizedIdentifierDescriptor;\n};\n\nexport type FindDocumentsParams = {\n  collectionName: string;\n  expressions: Expression[];\n  projection?: Projection;\n  sort?: SortDescriptor;\n  skip?: number;\n  limit?: number;\n};\n\nexport type CountDocumentsParams = {\n  collectionName: string;\n  expressions: Expression[];\n};\n\nexport type MigrateCollectionParams = {\n  collectionName: string;\n  collectionSchema: CollectionSchema;\n  silent?: boolean;\n};\n\nexport type MigrateCollectionResult = {\n  name: string;\n  createdIndexes: string[];\n  droppedIndexes: string[];\n};\n\nexport type CollectionSchema = {\n  indexes: CollectionIndex[];\n};\n\nexport type CollectionIndex = {\n  attributes: {[name: string]: SortDirection};\n  isPrimary: boolean;\n  isUnique: boolean;\n};\n\nexport type TraceEntry = {\n  operation: string;\n  params: any[];\n  result?: any;\n  error?: any;\n};\n\n/**\n * An abstract class from which classes such as [`MongoDBStore`](https://layrjs.com/docs/v2/reference/mongodb-store) or [`MemoryStore`](https://layrjs.com/docs/v2/reference/memory-store) are constructed. Unless you build a custom store, you probably won't have to use this class directly.\n */\nexport abstract class Store {\n  constructor(options = {}) {\n    assertNoUnknownOptions(options);\n  }\n\n  getURL(): string | undefined {\n    return undefined;\n  }\n\n  // === Root components ===\n\n  _rootComponents = new Set<typeof Component>();\n\n  /**\n   * Registers all the [storable components](https://layrjs.com/docs/v2/reference/storable#storable-component-class) that are provided (directly or recursively) by the specified root component.\n   *\n   * @param rootComponent A [`Component`](https://layrjs.com/docs/v2/reference/component) class.\n   *\n   * @example\n   * ```\n   * import {Component} from '﹫layr/component';\n   * import {Storable} from '﹫layr/storable';\n   * import {MongoDBStore} from '﹫layr/mongodb-store';\n   *\n   * class User extends Storable(Component) {\n   *   // ...\n   * }\n   *\n   * class Movie extends Storable(Component) {\n   *   // ...\n   * }\n   *\n   * class Application extends Component {\n   *   ﹫provide() static User = User;\n   *   ﹫provide() static Movie = Movie;\n   * }\n   *\n   * const store = new MongoDBStore('mongodb://user:pass@host:port/db');\n   *\n   * store.registerRootComponent(Application); // User and Movie will be registered\n   * ```\n   *\n   * @category Component Registration\n   */\n  registerRootComponent(rootComponent: typeof Component) {\n    assertIsComponentClass(rootComponent);\n\n    this._rootComponents.add(rootComponent);\n\n    for (const component of rootComponent.traverseComponents()) {\n      if (isStorableClass(component) && !component.isEmbedded()) {\n        this.registerStorable(component);\n      }\n    }\n  }\n\n  /**\n   * Gets all the root components that are registered into the store.\n   *\n   * @returns An iterator of [`Component`](https://layrjs.com/docs/v2/reference/component) classes.\n   *\n   * @category Component Registration\n   */\n  getRootComponents() {\n    return this._rootComponents.values();\n  }\n\n  // === Storables ===\n\n  _storables = new Map<string, typeof StorableComponent>();\n\n  /**\n   * Gets a [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class) that is registered into the store. An error is thrown if there is no storable component with the specified name.\n   *\n   * @param name The name of the storable component to get.\n   *\n   * @returns A [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storable-component-class) class.\n   *\n   * @example\n   * ```\n   * // See the definition of `store` in the `registerRootComponent()` example\n   *\n   * store.getStorable('Movie'); // => Movie class\n   * store.getStorable('User'); // => User class\n   * store.getStorable('Film'); // => Error\n   * ```\n   *\n   * @category Component Registration\n   */\n  getStorable(name: string) {\n    const storable = this._getStorable(name);\n\n    if (storable !== undefined) {\n      return storable;\n    }\n\n    throw new Error(`The storable component '${name}' is not registered in the store`);\n  }\n\n  /**\n   * Returns whether a [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class) is registered into the store.\n   *\n   * @param name The name of the storable component to check.\n   *\n   * @returns A boolean.\n   *\n   * @example\n   * ```\n   * // See the definition of `store` in the `registerRootComponent()` example\n   *\n   * store.hasStorable('Movie'); // => true\n   * store.hasStorable('User'); // => true\n   * store.hasStorable('Film'); // => false\n   * ```\n   *\n   * @category Component Registration\n   */\n  hasStorable(name: string) {\n    return this._getStorable(name) !== undefined;\n  }\n\n  _getStorable(name: string) {\n    return this._storables.get(name);\n  }\n\n  getStorableOfType(type: string) {\n    const storable = this._getStorableOfType(type);\n\n    if (storable !== undefined) {\n      return storable;\n    }\n\n    throw new Error(`The storable component of type '${type}' is not registered in the store`);\n  }\n\n  _getStorableOfType(type: string) {\n    const isComponentClassType = assertIsComponentType(type) === 'componentClassType';\n\n    const componentName = isComponentClassType\n      ? getComponentNameFromComponentClassType(type)\n      : getComponentNameFromComponentInstanceType(type);\n\n    const component = this._getStorable(componentName);\n\n    if (component === undefined) {\n      return undefined;\n    }\n\n    return isComponentClassType ? component : component.prototype;\n  }\n\n  /**\n   * Registers a specific [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class) into the store. Typically, instead of using this method, you would rather use the [`registerRootComponent()`](https://layrjs.com/docs/v2/reference/store#register-root-component-instance-method) method to register multiple storable components at once.\n   *\n   * @param storable The [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storable-component-class) class to register.\n   *\n   * @example\n   * ```\n   * class Movie extends Storable(Component) {\n   *   // ...\n   * }\n   *\n   * const store = new MongoDBStore('mongodb://user:pass@host:port/db');\n   *\n   * store.registerStorable(Movie);\n   * ```\n   *\n   * @category Component Registration\n   */\n  registerStorable(storable: typeof StorableComponent) {\n    assertIsStorableClass(storable);\n\n    if (storable.isEmbedded()) {\n      throw new Error(\n        `Cannot register an embedded storable component (${storable.describeComponent()})`\n      );\n    }\n\n    if (storable.hasStore()) {\n      if (storable.getStore() === this) {\n        return;\n      }\n\n      throw new Error(\n        `Cannot register a storable component that is already registered in another store (${storable.describeComponent()})`\n      );\n    }\n\n    const storableName = storable.getComponentName();\n\n    const existingStorable = this._storables.get(storableName);\n\n    if (existingStorable !== undefined) {\n      throw new Error(\n        `A storable component with the same name is already registered (${existingStorable.describeComponent()})`\n      );\n    }\n\n    storable.__setStore(this);\n\n    this._storables.set(storableName, storable);\n  }\n\n  /**\n   * Gets all the [storable components](https://layrjs.com/docs/v2/reference/storable#storable-component-class) that are registered into the store.\n   *\n   * @returns An iterator of [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storable-component-class) classes.\n   *\n   * @category Component Registration\n   */\n  getStorables() {\n    return this._storables.values();\n  }\n\n  // === Collections ===\n\n  _getCollectionNameFromStorable(storable: typeof StorableComponent | StorableComponent) {\n    return ensureComponentClass(storable).getComponentName();\n  }\n\n  // === Storable operations ===\n\n  async load(\n    storable: StorableComponent,\n    options: {attributeSelector?: AttributeSelector; throwIfMissing?: boolean} = {}\n  ) {\n    return await this._runOperation('load', [storable, options], async () => {\n      let {attributeSelector = true, throwIfMissing = true} = options;\n\n      const identifierDescriptor = storable.getIdentifierDescriptor();\n\n      // Always include the identifier attribute\n      const identifierAttributeSelector = createAttributeSelectorFromNames(\n        Object.keys(identifierDescriptor)\n      );\n      attributeSelector = mergeAttributeSelectors(attributeSelector, identifierAttributeSelector);\n\n      const collectionName = this._getCollectionNameFromStorable(storable);\n\n      const documentIdentifierDescriptor = this.toDocument(storable, identifierDescriptor);\n      const documentAttributeSelector = this.toDocument(storable, attributeSelector);\n      const projection = buildProjection(documentAttributeSelector);\n\n      let document = await this.readDocument({\n        collectionName,\n        identifierDescriptor: documentIdentifierDescriptor,\n        projection\n      });\n\n      if (document !== undefined) {\n        document = pickFromAttributeSelector(document, documentAttributeSelector, {\n          includeAttributeNames: ['__component']\n        });\n\n        const serializedStorable = this.fromDocument(storable, document);\n\n        const loadedStorable = storable.deserialize(serializedStorable, {source: 'store'});\n\n        return loadedStorable;\n      }\n\n      if (!throwIfMissing) {\n        return undefined;\n      }\n\n      throw Object.assign(\n        new Error(\n          `Cannot load a component that is missing from the store (${storable.describeComponent()}, ${ensureComponentClass(\n            storable\n          ).describeIdentifierDescriptor(identifierDescriptor)})`\n        ),\n        {code: 'COMPONENT_IS_MISSING_FROM_STORE', expose: true}\n      );\n    });\n  }\n\n  async save(\n    storable: StorableComponent,\n    options: {\n      attributeSelector?: AttributeSelector;\n      throwIfMissing?: boolean;\n      throwIfExists?: boolean;\n    } = {}\n  ) {\n    return await this._runOperation('save', [storable, options], async () => {\n      const isNew = storable.isNew();\n\n      const {attributeSelector = true, throwIfMissing = !isNew, throwIfExists = isNew} = options;\n\n      if (throwIfMissing === true && throwIfExists === true) {\n        throw new Error(\n          \"The 'throwIfMissing' and 'throwIfExists' options cannot be both set to true\"\n        );\n      }\n\n      storable._assertArrayItemsAreFullyLoaded(attributeSelector);\n\n      storable.validate(attributeSelector);\n\n      const collectionName = this._getCollectionNameFromStorable(storable);\n\n      const identifierDescriptor = storable.getIdentifierDescriptor();\n      const documentIdentifierDescriptor = this.toDocument(storable, identifierDescriptor);\n\n      const serializedStorable = storable.serialize({attributeSelector, includeIsNewMarks: false})!;\n      const document = this.toDocument(storable, serializedStorable);\n\n      let wasSaved: boolean;\n\n      try {\n        if (isNew) {\n          deleteUndefinedProperties(document);\n\n          wasSaved = await this.createDocument({\n            collectionName,\n            identifierDescriptor: documentIdentifierDescriptor,\n            document\n          });\n        } else {\n          const documentPatch = buildDocumentPatch(document);\n\n          wasSaved = await this.updateDocument({\n            collectionName,\n            identifierDescriptor: documentIdentifierDescriptor,\n            documentPatch\n          });\n        }\n      } catch (error: any) {\n        const {code, indexName} = error;\n\n        if (code === 'DUPLICATE_KEY_ERROR') {\n          throw Object.assign(\n            new Error(\n              `Cannot save a component with an attribute value that should be unique but already exists in the store (${storable.describeComponent()}, ${ensureComponentClass(\n                storable\n              ).describeIdentifierDescriptor(identifierDescriptor)}, index: '${indexName}')`\n            ),\n            {code: 'UNIQUE_ATTRIBUTE_ALREADY_EXISTS_IN_STORE', indexName, expose: true}\n          );\n        }\n\n        throw error;\n      }\n\n      if (!wasSaved) {\n        if (throwIfMissing) {\n          throw Object.assign(\n            new Error(\n              `Cannot save a non-new component that is missing from the store (${storable.describeComponent()}, ${ensureComponentClass(\n                storable\n              ).describeIdentifierDescriptor(identifierDescriptor)})`\n            ),\n            {code: 'COMPONENT_IS_MISSING_FROM_STORE', expose: true}\n          );\n        }\n\n        if (throwIfExists) {\n          throw Object.assign(\n            new Error(\n              `Cannot save a new component that already exists in the store (${storable.describeComponent()}, ${ensureComponentClass(\n                storable\n              ).describeIdentifierDescriptor(identifierDescriptor)})`\n            ),\n            {code: 'COMPONENT_ALREADY_EXISTS_IN_STORE', expose: true}\n          );\n        }\n\n        return undefined;\n      }\n\n      if (isNew) {\n        storable.markAsNotNew(); // TODO: Mark also embedded components as not new\n      }\n\n      storable.traverseAttributes(\n        (attribute) => {\n          attribute.setValueSource('store');\n        },\n        {attributeSelector, setAttributesOnly: true}\n      );\n\n      return storable;\n    });\n  }\n\n  async delete(storable: StorableComponent, options: {throwIfMissing?: boolean} = {}) {\n    return await this._runOperation('delete', [storable, options], async () => {\n      const {throwIfMissing = true} = options;\n\n      const collectionName = this._getCollectionNameFromStorable(storable);\n\n      const identifierDescriptor = storable.getIdentifierDescriptor();\n      const documentIdentifierDescriptor = this.toDocument(storable, identifierDescriptor);\n\n      const wasDeleted = await this.deleteDocument({\n        collectionName,\n        identifierDescriptor: documentIdentifierDescriptor\n      });\n\n      if (!wasDeleted) {\n        if (throwIfMissing) {\n          throw Object.assign(\n            new Error(\n              `Cannot delete a component that is missing from the store (${storable.describeComponent()}, ${ensureComponentClass(\n                storable\n              ).describeIdentifierDescriptor(identifierDescriptor)})`\n            ),\n            {code: 'COMPONENT_IS_MISSING_FROM_STORE', expose: true}\n          );\n        }\n\n        return undefined;\n      }\n\n      return storable;\n    });\n  }\n\n  async find(\n    storable: typeof StorableComponent,\n    query: Query = {},\n    options: {sort?: SortDescriptor; skip?: number; limit?: number} = {}\n  ) {\n    return await this._runOperation('find', [storable, query, options], async () => {\n      const {sort = {}, skip, limit} = options;\n\n      const storablePrototype = storable.prototype;\n\n      const collectionName = this._getCollectionNameFromStorable(storablePrototype);\n\n      const serializedQuery = simpleSerialize(query);\n      const documentExpressions = this.toDocumentExpressions(storablePrototype, serializedQuery);\n\n      const documentSort = this.toDocument(storablePrototype, sort);\n\n      const primaryIdentifierAttribute = storablePrototype.getPrimaryIdentifierAttribute();\n      const attributeSelector = {[primaryIdentifierAttribute.getName()]: true};\n      const documentAttributeSelector = this.toDocument(storablePrototype, attributeSelector);\n      const projection = buildProjection(documentAttributeSelector);\n\n      let documents = await this.findDocuments({\n        collectionName,\n        expressions: documentExpressions,\n        projection,\n        sort: documentSort,\n        skip,\n        limit\n      });\n\n      documents = documents.map((document) =>\n        pickFromAttributeSelector(document, documentAttributeSelector, {\n          includeAttributeNames: ['__component']\n        })\n      );\n\n      const serializedStorables = documents.map((document) =>\n        this.fromDocument(storable, document)\n      );\n\n      const foundStorables: StorableComponent[] = [];\n\n      for (const serializedStorable of serializedStorables) {\n        foundStorables.push(\n          (await deserialize(serializedStorable, {\n            rootComponent: storable,\n            source: 'store'\n          })) as StorableComponent\n        );\n      }\n\n      return foundStorables;\n    });\n  }\n\n  async count(storable: typeof StorableComponent, query: Query = {}) {\n    return await this._runOperation('count', [storable, query], async () => {\n      const storablePrototype = storable.prototype;\n\n      const collectionName = this._getCollectionNameFromStorable(storablePrototype);\n\n      const serializedQuery = simpleSerialize(query);\n      const documentExpressions = this.toDocumentExpressions(storablePrototype, serializedQuery);\n\n      const documentsCount = await this.countDocuments({\n        collectionName,\n        expressions: documentExpressions\n      });\n\n      return documentsCount;\n    });\n  }\n\n  async _runOperation<\n    PromiseResult extends Promise<unknown>,\n    Result = PromiseLikeValue<PromiseResult>\n  >(operation: string, params: any[], func: () => PromiseResult): Promise<Result> {\n    const trace = this._trace;\n\n    try {\n      const result = await func();\n\n      if (trace !== undefined) {\n        trace.push({operation, params, result});\n      }\n\n      return result as Result;\n    } catch (error) {\n      if (trace !== undefined) {\n        trace.push({operation, params, error});\n      }\n\n      throw error;\n    }\n  }\n\n  // === Tracing ===\n\n  _trace: TraceEntry[] | undefined;\n\n  getTrace() {\n    const trace = this._trace;\n\n    if (trace === undefined) {\n      throw new Error('The store is not currently tracing');\n    }\n\n    return trace;\n  }\n\n  startTrace() {\n    this._trace = [];\n  }\n\n  stopTrace() {\n    this._trace = undefined;\n  }\n\n  // === Abstract collection operations ===\n\n  abstract migrateCollection({\n    collectionName,\n    collectionSchema,\n    silent\n  }: MigrateCollectionParams): Promise<MigrateCollectionResult>;\n\n  // === Abstract document operations ===\n\n  abstract createDocument({\n    collectionName,\n    identifierDescriptor,\n    document\n  }: CreateDocumentParams): Promise<boolean>;\n\n  abstract readDocument({\n    collectionName,\n    identifierDescriptor,\n    projection\n  }: ReadDocumentParams): Promise<Document | undefined>;\n\n  abstract updateDocument({\n    collectionName,\n    identifierDescriptor,\n    documentPatch\n  }: UpdateDocumentParams): Promise<boolean>;\n\n  abstract deleteDocument({\n    collectionName,\n    identifierDescriptor\n  }: DeleteDocumentParams): Promise<boolean>;\n\n  abstract findDocuments({\n    collectionName,\n    expressions,\n    projection,\n    sort,\n    skip,\n    limit\n  }: FindDocumentsParams): Promise<Document[]>;\n\n  abstract countDocuments({collectionName, expressions}: CountDocumentsParams): Promise<number>;\n\n  // === Serialization ===\n\n  toDocument<Value>(_storable: typeof StorableComponent | StorableComponent, value: Value) {\n    return simpleDeserialize(value) as Value;\n  }\n\n  // {a: 1, b: {c: 2}} => [['a', '$equal', 1], ['b.c', '$equal', 2]]\n  toDocumentExpressions(storable: typeof StorableComponent | StorableComponent, query: Query) {\n    const documentQuery = this.toDocument(storable, query);\n\n    const build = function (query: Query, expressions: Expression[], path: Path) {\n      for (const [name, value] of Object.entries(query)) {\n        if (looksLikeOperator(name)) {\n          const operator = name;\n\n          if (operator === '$and' || operator === '$or' || operator === '$nor') {\n            handleOperator(operator, value, expressions, path, {query});\n            continue;\n          }\n\n          throw new Error(\n            `A query cannot contain the operator '${operator}' at its root (query: ${JSON.stringify(\n              query\n            )})`\n          );\n        }\n\n        const subpath: Path = path !== '' ? `${path}.${name}` : name;\n\n        handleValue(value, expressions, subpath, {query});\n      }\n    };\n\n    const handleValue = function (\n      value: AttributeValue | object,\n      expressions: Expression[],\n      subpath: Path,\n      {query}: {query: Query}\n    ) {\n      if (!isPlainObject(value)) {\n        // Make '$equal' the default operator for non object values\n        expressions.push([subpath, '$equal', value]);\n        return;\n      }\n\n      const object = value;\n\n      let objectContainsAttributes = false;\n      let objectContainsOperators = false;\n\n      for (const name of Object.keys(object)) {\n        if (looksLikeOperator(name)) {\n          objectContainsOperators = true;\n        } else {\n          objectContainsAttributes = true;\n        }\n      }\n\n      if (objectContainsAttributes) {\n        if (objectContainsOperators) {\n          throw new Error(\n            `A subquery cannot contain both an attribute and an operator (subquery: ${JSON.stringify(\n              object\n            )})`\n          );\n        }\n\n        const subquery = object;\n        build(subquery, expressions, subpath);\n        return;\n      }\n\n      if (objectContainsOperators) {\n        const operators = object;\n\n        for (const [operator, value] of Object.entries(operators)) {\n          handleOperator(operator, value, expressions, subpath, {query});\n        }\n      }\n    };\n\n    const handleOperator = function (\n      operator: Operator,\n      value: AttributeValue | object,\n      expressions: Expression[],\n      path: Path,\n      {query}: {query: Query}\n    ) {\n      const normalizedOperator = normalizeOperatorForValue(operator, value, {query});\n\n      if (\n        normalizedOperator === '$some' ||\n        normalizedOperator === '$every' ||\n        normalizedOperator === '$not'\n      ) {\n        const subexpressions: Expression[] = [];\n        handleValue(value, subexpressions, '', {query});\n        expressions.push([path, normalizedOperator, subexpressions]);\n        return;\n      }\n\n      if (\n        normalizedOperator === '$and' ||\n        normalizedOperator === '$or' ||\n        normalizedOperator === '$nor'\n      ) {\n        const values = value as (AttributeValue | object)[];\n        const operatorExpressions = values.map((value) => {\n          const subexpressions: Expression[] = [];\n          handleValue(value, subexpressions, '', {query});\n          return subexpressions;\n        });\n        expressions.push([path, normalizedOperator, operatorExpressions]);\n        return;\n      }\n\n      if (isPlainObject(value)) {\n        throw new Error(\n          `Unexpected object encountered in a query (query: ${JSON.stringify(query)})`\n        );\n      }\n\n      expressions.push([path, normalizedOperator, value]);\n    };\n\n    const documentExpressions: Expression[] = [];\n    build(documentQuery, documentExpressions, '');\n    return documentExpressions;\n  }\n\n  fromDocument(\n    _storable: typeof StorableComponent | StorableComponent,\n    document: Document\n  ): Document {\n    return simpleSerialize(document);\n  }\n\n  // === Migration ===\n\n  /**\n   * Migrates the database to reflect all the [storable components](https://layrjs.com/docs/v2/reference/storable#storable-component-class) that are registered into the store.\n   *\n   * The migration consists in synchronizing the indexes of the database with the indexes that are defined in each storable component (typically using the [`@index()`](https://layrjs.com/docs/v2/reference/storable#index-decorator) decorator).\n   *\n   * @param [options.silent] A boolean specifying whether the operation should not produce any output in the console (default: `false`).\n   *\n   * @examplelink See an example of use in the [`Index`](https://layrjs.com/docs/v2/reference/index) class.\n   *\n   * @category Migration\n   */\n  async migrateStorables(options: {silent?: boolean} = {}) {\n    const {silent = false} = options;\n\n    const allResults: {collections: MigrateCollectionResult[]} = {\n      collections: []\n    };\n\n    for (const storable of this.getStorables()) {\n      const result = await this.migrateStorable(storable, {silent});\n      allResults.collections.push(result);\n    }\n\n    return allResults;\n  }\n\n  async migrateStorable(storable: typeof StorableComponent, options: {silent?: boolean} = {}) {\n    const {silent = false} = options;\n\n    const collectionName = this._getCollectionNameFromStorable(storable.prototype);\n    const collectionSchema = this.getCollectionSchema(storable.prototype);\n\n    for (const index of collectionSchema.indexes) {\n      index.attributes = this.toDocument(storable.prototype, index.attributes);\n    }\n\n    const result = await this.migrateCollection({collectionName, collectionSchema, silent});\n\n    return result;\n  }\n\n  getCollectionSchema(storable: StorableComponent) {\n    const schema: CollectionSchema = {\n      indexes: this._getCollectionIndexes(storable)\n    };\n\n    return schema;\n  }\n\n  _getCollectionIndexes(storable: StorableComponent) {\n    // TODO: Reimplement this method from scratch make it more maintainable\n\n    const indexes: CollectionIndex[] = [];\n\n    const resolveAttributeName = (name: string) => {\n      // If the the type of the attribute is a referenced component,\n      // return the concatenation of attribute name with the referenced component\n      // primary identifier attribute name\n\n      const attribute = storable.getAttribute(name);\n      const scalarType = attribute.getValueType().getScalarType();\n\n      if (isComponentValueTypeInstance(scalarType)) {\n        const component = scalarType.getComponent(attribute);\n\n        if (\n          isComponentInstance(component) &&\n          !component.constructor.isEmbedded() &&\n          component.hasPrimaryIdentifierAttribute()\n        ) {\n          return name + '.' + component.getPrimaryIdentifierAttribute({autoFork: false}).getName();\n        }\n      }\n\n      return name;\n    };\n\n    for (const attribute of storable.getAttributes()) {\n      if (isIdentifierAttributeInstance(attribute)) {\n        const isEmbedded = storable.constructor.isEmbedded();\n\n        const attributes = {[attribute.getName()]: 'asc' as SortDirection};\n        const isPrimary = !isEmbedded && isPrimaryIdentifierAttributeInstance(attribute);\n        const isUnique = !isEmbedded;\n\n        indexes.push({attributes, isPrimary, isUnique});\n\n        continue;\n      }\n\n      const name = attribute.getName();\n      const resolvedName = resolveAttributeName(name);\n\n      if (name !== resolvedName) {\n        // It means that the type of the attribute is a referenced component\n        // with a primary identifier attribute\n\n        const attributes = {[resolvedName]: 'asc' as SortDirection};\n        const isPrimary = false;\n        const isUnique = false;\n\n        indexes.push({attributes, isPrimary, isUnique});\n\n        continue;\n      }\n\n      const scalarType = attribute.getValueType().getScalarType();\n\n      if (isComponentValueTypeInstance(scalarType)) {\n        const component = scalarType.getComponent(attribute);\n\n        if (isStorableInstance(component) && component.constructor.isEmbedded()) {\n          const subindexes = this._getCollectionIndexes(component);\n\n          for (const {attributes, isPrimary, isUnique} of subindexes) {\n            indexes.push({\n              attributes: mapKeys(attributes, (_value, subname) => name + '.' + subname),\n              isPrimary,\n              isUnique\n            });\n          }\n        }\n      }\n    }\n\n    for (const index of storable.getIndexes({autoFork: false})) {\n      const attributes = mapKeys(index.getAttributes(), (_value, name) =>\n        resolveAttributeName(name)\n      );\n      const isPrimary = false;\n      const isUnique = Boolean(index.getOptions().isUnique);\n\n      indexes.push({attributes, isPrimary, isUnique});\n    }\n\n    return indexes;\n  }\n\n  // === Utilities ===\n\n  static isStore(value: any): value is Store {\n    return isStoreInstance(value);\n  }\n}\n"
  },
  {
    "path": "packages/store/src/utilities.ts",
    "content": "import type {Store} from './store';\n\nexport function isStoreClass(value: any): value is typeof Store {\n  return typeof value?.isStore === 'function';\n}\n\nexport function isStoreInstance(value: any): value is Store {\n  return typeof value?.constructor?.isStore === 'function';\n}\n"
  },
  {
    "path": "packages/store/tsconfig.json",
    "content": "{\n  \"extends\": \"@mvila/tsconfig\",\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/utilities/README.md",
    "content": "# @layr/utilities\n\nA set of basic utilities used by Layr.\n\n## Installation\n\n```\nnpm install @layr/utilities\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/utilities/package.json",
    "content": "{\n  \"name\": \"@layr/utilities\",\n  \"version\": \"1.0.9\",\n  \"description\": \"A set of basic utilities used by Layr\",\n  \"keywords\": [\n    \"layr\",\n    \"utilities\",\n    \"utility\",\n    \"tool\",\n    \"function\"\n  ],\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/layrjs/layr/tree/master/packages/utilities\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"dist/node-cjs/index.js\",\n  \"module\": \"dist/node-esm/index.js\",\n  \"engines\": {\n    \"node\": \">=16.0.0\"\n  },\n  \"scripts\": {\n    \"build\": \"dev-tools build:ts-library\",\n    \"prepare\": \"npm run build\",\n    \"test\": \"dev-tools test:ts-library\",\n    \"publish:package\": \"dev-tools publish:package\",\n    \"update\": \"dev-tools update:dependencies\"\n  },\n  \"dependencies\": {\n    \"tslib\": \"^2.4.1\"\n  },\n  \"devDependencies\": {\n    \"@mvila/dev-tools\": \"^1.3.1\",\n    \"@mvila/tsconfig\": \"^1.0.6\",\n    \"@types/jest\": \"^29.2.5\"\n  }\n}\n"
  },
  {
    "path": "packages/utilities/src/error.test.ts",
    "content": "import {\n  createError,\n  throwError,\n  formatError,\n  DEFAULT_DISPLAY_MESSAGE,\n  ExtendedError\n} from './error';\n\ndescribe('Error', () => {\n  test('createError()', async () => {\n    let error = createError();\n\n    expect(error instanceof Error).toBe(true);\n    expect(error.message).toBe('');\n    expect('displayMessage' in error).toBe(false);\n    expect('code' in error).toBe(false);\n\n    error = createError('Movie not found');\n\n    expect(error instanceof Error).toBe(true);\n    expect(error.message).toBe('Movie not found');\n    expect('displayMessage' in error).toBe(false);\n    expect('code' in error).toBe(false);\n\n    error = createError('Movie not found', {displayMessage: 'Sorry, the specified was not found.'});\n\n    expect(error instanceof Error).toBe(true);\n    expect(error.message).toBe('Movie not found');\n    expect(error.displayMessage).toBe('Sorry, the specified was not found.');\n    expect('code' in error).toBe(false);\n\n    error = createError('Movie not found', {displayMessage: undefined, code: 'MOVIE_NOT_FOUND'});\n\n    expect(error instanceof Error).toBe(true);\n    expect(error.message).toBe('Movie not found');\n    expect('displayMessage' in error).toBe(false);\n    expect(error.code).toBe('MOVIE_NOT_FOUND');\n\n    error = createError('Movie not found', {id: 'abc123'});\n\n    expect(error instanceof Error).toBe(true);\n    expect(error.message).toBe('Movie not found');\n    expect('displayMessage' in error).toBe(false);\n    expect('code' in error).toBe(false);\n    expect(error.id).toBe('abc123');\n  });\n\n  test('throwError()', async () => {\n    let error: ExtendedError;\n\n    try {\n      throwError('Movie not found', {displayMessage: 'Sorry, the specified was not found.'});\n    } catch (err: any) {\n      error = err;\n    }\n\n    expect(error instanceof Error).toBe(true);\n    expect(error.message).toBe('Movie not found');\n    expect(error.displayMessage).toBe('Sorry, the specified was not found.');\n    expect('code' in error).toBe(false);\n  });\n\n  test('formatError()', async () => {\n    expect(formatError()).toBe(DEFAULT_DISPLAY_MESSAGE);\n    expect(formatError(new Error())).toBe(DEFAULT_DISPLAY_MESSAGE);\n    expect(formatError(new Error('Movie not found'))).toBe(DEFAULT_DISPLAY_MESSAGE);\n    expect(\n      formatError(\n        createError('Movie not found', {displayMessage: 'Sorry, the specified was not found.'})\n      )\n    ).toBe('Sorry, the specified was not found.');\n  });\n});\n"
  },
  {
    "path": "packages/utilities/src/error.ts",
    "content": "export const DEFAULT_DISPLAY_MESSAGE = 'Sorry, something went wrong.';\n\nexport type ExtendedError = Error & ExtendedErrorAttributes;\n\ntype ExtendedErrorAttributes = Record<string, any> & {\n  displayMessage?: string;\n  code?: number | string;\n};\n\nexport function createError(message?: string, attributes: ExtendedErrorAttributes = {}) {\n  const error = new Error(message) as ExtendedError;\n\n  for (const [name, value] of Object.entries(attributes)) {\n    if (value !== undefined) {\n      error[name] = value;\n    }\n  }\n\n  return error;\n}\n\nexport function throwError(message?: string, attributes: ExtendedErrorAttributes = {}): never {\n  throw createError(message, attributes);\n}\n\nexport function formatError(\n  error?: ExtendedError,\n  {defaultDisplayMessage = DEFAULT_DISPLAY_MESSAGE} = {}\n) {\n  return error?.displayMessage ?? defaultDisplayMessage;\n}\n"
  },
  {
    "path": "packages/utilities/src/index.ts",
    "content": "export * from './error';\nexport * from './time';\n"
  },
  {
    "path": "packages/utilities/src/time.ts",
    "content": "export const MILLISECOND = 1;\nexport const SECOND = 1000 * MILLISECOND;\nexport const MINUTE = 60 * SECOND;\nexport const HOUR = 60 * MINUTE;\nexport const DAY = 24 * HOUR;\nexport const WEEK = 7 * DAY;\nexport const MONTH = 30.4375 * DAY;\nexport const YEAR = 12 * MONTH;\n\nexport function sleep(milliseconds: number) {\n  return new Promise<void>((resolve) => {\n    setTimeout(resolve, milliseconds);\n  });\n}\n"
  },
  {
    "path": "packages/utilities/tsconfig.json",
    "content": "{\n  \"extends\": \"@mvila/tsconfig\",\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/with-roles/.gitignore",
    "content": ".DS_STORE\nnode_modules\n*.log\n/dist\n"
  },
  {
    "path": "packages/with-roles/README.md",
    "content": "# @layr/with-roles\n\nSimple role system for Layr.\n\n## Installation\n\n```\nnpm install @layr/with-roles\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/with-roles/package.json",
    "content": "{\n  \"name\": \"@layr/with-roles\",\n  \"version\": \"2.0.59\",\n  \"description\": \"Simple role system for Layr\",\n  \"keywords\": [\n    \"layr\",\n    \"component\",\n    \"attribute\",\n    \"method\",\n    \"role\",\n    \"authorization\",\n    \"security\"\n  ],\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/layrjs/layr/tree/master/packages/with-roles\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"main\": \"dist/node-cjs/index.js\",\n  \"module\": \"dist/node-esm/index.js\",\n  \"engines\": {\n    \"node\": \">=16.0.0\"\n  },\n  \"scripts\": {\n    \"build\": \"dev-tools build:ts-library\",\n    \"prepare\": \"npm run build\",\n    \"test\": \"dev-tools test:ts-library\",\n    \"publish:package\": \"dev-tools publish:package\",\n    \"update\": \"dev-tools update:dependencies\"\n  },\n  \"dependencies\": {\n    \"@layr/component\": \"^2.0.51\",\n    \"core-helpers\": \"^1.0.8\",\n    \"possibly-async\": \"^1.0.7\",\n    \"tslib\": \"^2.4.1\"\n  },\n  \"devDependencies\": {\n    \"@mvila/dev-tools\": \"^1.3.1\",\n    \"@mvila/tsconfig\": \"^1.0.6\",\n    \"@types/jest\": \"^29.2.5\"\n  }\n}\n"
  },
  {
    "path": "packages/with-roles/src/decorators.test.ts",
    "content": "import {Component} from '@layr/component';\n\nimport {WithRoles} from './with-roles';\nimport {role} from './decorators';\nimport {isRoleInstance} from './role';\n\ndescribe('Decorators', () => {\n  test('@role()', async () => {\n    class Movie extends WithRoles(Component) {\n      @role('anyone') static anyoneRoleResolver() {\n        return true;\n      }\n\n      @role('author') authorRoleResolver() {\n        return false;\n      }\n    }\n\n    const anyoneRole = Movie.getRole('anyone');\n\n    expect(isRoleInstance(anyoneRole)).toBe(true);\n    expect(anyoneRole.getName()).toBe('anyone');\n    expect(anyoneRole.getParent()).toBe(Movie);\n    expect(anyoneRole.getResolver()).toBe(Movie.anyoneRoleResolver);\n\n    const authorRole = Movie.prototype.getRole('author');\n\n    expect(isRoleInstance(authorRole)).toBe(true);\n    expect(authorRole.getName()).toBe('author');\n    expect(authorRole.getParent()).toBe(Movie.prototype);\n    expect(authorRole.getResolver()).toBe(Movie.prototype.authorRoleResolver);\n  });\n});\n"
  },
  {
    "path": "packages/with-roles/src/decorators.ts",
    "content": "import {ComponentWithRoles} from './with-roles';\nimport {isComponentWithRolesClassOrInstance} from './utilities';\n\n/**\n * Defines a [role](https://layrjs.com/docs/v2/reference/role) in a [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class) class or prototype.\n *\n * This decorator should be used to decorate a class or instance method that implements the role's resolver. The method can be asynchronous and should return a boolean indicating whether a user has the corresponding role.\n *\n * @param name The name of the role to define.\n *\n * @examplelink See an example of use in the [`WithRoles()`](https://layrjs.com/docs/v2/reference/with-roles#with-roles-mixin) mixin.\n *\n * @category Decorators\n * @decorator\n */\nexport function role(name: string) {\n  return function (\n    target: typeof ComponentWithRoles | ComponentWithRoles,\n    _key: string,\n    descriptor: PropertyDescriptor\n  ) {\n    const {value: resolver, enumerable} = descriptor;\n\n    if (\n      !(\n        isComponentWithRolesClassOrInstance(target) &&\n        typeof resolver === 'function' &&\n        enumerable === false\n      )\n    ) {\n      throw new Error(\n        `@role() should be used to decorate a component with roles method (property: '${name}')`\n      );\n    }\n\n    target.setRole(name, resolver);\n  };\n}\n"
  },
  {
    "path": "packages/with-roles/src/index.ts",
    "content": "export * from './decorators';\nexport * from './role';\nexport * from './utilities';\nexport * from './with-roles';\n"
  },
  {
    "path": "packages/with-roles/src/role.test.ts",
    "content": "import {Component} from '@layr/component';\n\nimport {WithRoles} from './with-roles';\nimport {Role, isRoleInstance} from './role';\n\ndescribe('Role', () => {\n  test('new Role()', async () => {\n    class Movie extends WithRoles(Component) {}\n\n    const resolver = () => true;\n\n    const role = new Role('anyone', Movie, resolver);\n\n    expect(isRoleInstance(role)).toBe(true);\n\n    expect(role.getName()).toBe('anyone');\n    expect(role.getParent()).toBe(Movie);\n    expect(role.getResolver()).toBe(resolver);\n  });\n\n  test('resolve()', async () => {\n    class Movie extends WithRoles(Component) {}\n\n    const resolver = jest.fn(async function (this: unknown) {\n      expect(this).toBe(Movie);\n\n      return true;\n    });\n\n    const role = new Role('anyone', Movie, resolver);\n\n    expect(resolver).toHaveBeenCalledTimes(0);\n\n    let result = await role.resolve();\n\n    expect(result).toBe(true);\n    expect(resolver).toHaveBeenCalledTimes(1);\n\n    // Let's make sure the resolver result has been cached\n    result = await role.resolve();\n\n    expect(result).toBe(true);\n    expect(resolver).toHaveBeenCalledTimes(1);\n  });\n\n  test('fork() and isForkOf()', async () => {\n    class Movie extends WithRoles(Component) {}\n\n    const resolver = () => true;\n\n    const role = new Role('anyone', Movie, resolver);\n\n    expect(role.getName()).toBe('anyone');\n    expect(role.getParent()).toBe(Movie);\n    expect(role.getResolver()).toBe(resolver);\n\n    const MovieFork = Movie.fork();\n\n    const roleFork = role.fork(MovieFork);\n\n    expect(roleFork).not.toBe(role);\n    expect(roleFork.getName()).toBe('anyone');\n    expect(roleFork.getParent()).toBe(MovieFork);\n    expect(roleFork.getResolver()).toBe(resolver);\n\n    expect(roleFork.isForkOf(role)).toBe(true);\n    expect(role.isForkOf(roleFork)).toBe(false);\n  });\n});\n"
  },
  {
    "path": "packages/with-roles/src/role.ts",
    "content": "import {possiblyAsync} from 'possibly-async';\nimport {\n  isPrototypeOf,\n  assertIsString,\n  assertIsFunction,\n  PromiseLikeable,\n  getTypeOf\n} from 'core-helpers';\nimport {inspect} from 'util';\n\nimport type {ComponentWithRoles} from './with-roles';\nimport {assertIsComponentWithRolesClassOrInstance} from './utilities';\n\nexport type RoleResolver = () => PromiseLikeable<boolean | undefined>;\n\n/**\n * Represents a role in a [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class) class or prototype.\n *\n * A role is composed of:\n *\n * - A name.\n * - A parent which should be a [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class) class or prototype.\n * - A resolver which should be a function returning a boolean indicating whether a user has the corresponding role.\n *\n * #### Usage\n *\n * Typically, you create a `Role` by using the [`@role()`](https://layrjs.com/docs/v2/reference/with-roles#role-decorator) decorator.\n *\n * See an example of use in the [`WithRoles()`](https://layrjs.com/docs/v2/reference/with-roles#with-roles-mixin) mixin.\n */\nexport class Role {\n  _name: string;\n  _parent: typeof ComponentWithRoles | ComponentWithRoles;\n  _resolver: RoleResolver;\n\n  /**\n   * Creates an instance of [`Role`](https://layrjs.com/docs/v2/reference/role).\n   *\n   * Typically, instead of using this constructor, you would rather use the [`@role()`](https://layrjs.com/docs/v2/reference/with-roles#role-decorator) decorator.\n   *\n   * @param name The name of the role.\n   * @param parent The parent of the role which should be a [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class) class or prototype.\n   * @param resolver A function that should return a boolean indicating whether a user has the corresponding role. The function can be asynchronous and is called with the current class or instance as `this` context.\n   *\n   * @returns The [`Role`](https://layrjs.com/docs/v2/reference/role) instance that was created.\n   *\n   * @example\n   * ```\n   * const role = new Role('admin', Article, function () {\n   *  // ...\n   * });\n   * ```\n   *\n   * @category Creation\n   */\n  constructor(\n    name: string,\n    parent: typeof ComponentWithRoles | ComponentWithRoles,\n    resolver: RoleResolver\n  ) {\n    assertIsString(name);\n    assertIsComponentWithRolesClassOrInstance(parent);\n    assertIsFunction(resolver);\n\n    this._name = name;\n    this._parent = parent;\n    this._resolver = resolver;\n  }\n\n  /**\n   * Returns the name of the role.\n   *\n   * @returns A string.\n   *\n   * @category Methods\n   */\n  getName() {\n    return this._name;\n  }\n\n  /**\n   * Returns the parent of the role.\n   *\n   * @returns A [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class) class or instance.\n   *\n   * @category Methods\n   */\n  getParent() {\n    return this._parent;\n  }\n\n  /**\n   * Returns the resolver function of the role.\n   *\n   * @returns A function.\n   *\n   * @category Methods\n   */\n  getResolver() {\n    return this._resolver;\n  }\n\n  _resolvedValue?: boolean | undefined;\n  _hasBeenResolved?: boolean;\n\n  /**\n   * Resolves the role by calling its resolver function.\n   *\n   * The resolver function is called with the role's parent as `this` context.\n   *\n   * Once a role has been resolved, the result is cached, so the resolver function is called only one time.\n   *\n   * @returns A boolean.\n   *\n   * @category Methods\n   * @possiblyasync\n   */\n  resolve(): PromiseLikeable<boolean | undefined> {\n    if (this._hasBeenResolved) {\n      return this._resolvedValue;\n    }\n\n    return possiblyAsync(this._resolver.call(this._parent), (resolvedValue) => {\n      this._resolvedValue = resolvedValue;\n      this._hasBeenResolved = true;\n      return resolvedValue;\n    });\n  }\n\n  fork(parent: typeof ComponentWithRoles | ComponentWithRoles) {\n    const roleFork = Object.create(this) as Role;\n\n    roleFork._parent = parent;\n\n    return roleFork;\n  }\n\n  isForkOf(role: Role) {\n    assertIsRoleInstance(role);\n\n    return isPrototypeOf(role, this);\n  }\n\n  static isRole(value: any): value is Role {\n    return isRoleInstance(value);\n  }\n\n  [inspect.custom]() {\n    return {name: this._name, resolver: this._resolver};\n  }\n}\n\n/**\n * Returns whether the specified value is a [`Role`](https://layrjs.com/docs/v2/reference/role) instance.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isRoleInstance(value: any): value is Role {\n  return typeof value?.constructor?.isRole === 'function';\n}\n\n/**\n * Throws an error if the specified value is not a [`Role`](https://layrjs.com/docs/v2/reference/role) instance.\n *\n * @param value A value of any type.\n *\n * @category Utilities\n */\nexport function assertIsRoleInstance(value: any): asserts value is Role {\n  if (!isRoleInstance(value)) {\n    throw new Error(`Expected a role instance, but received a value of type '${getTypeOf(value)}'`);\n  }\n}\n"
  },
  {
    "path": "packages/with-roles/src/utilities.ts",
    "content": "import {getTypeOf} from 'core-helpers';\n\nimport type {ComponentWithRoles} from './with-roles';\n\n/**\n * Returns whether the specified value is a [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class) class or instance.\n *\n * @param value A value of any type.\n *\n * @returns A boolean.\n *\n * @category Utilities\n */\nexport function isComponentWithRolesClassOrInstance(\n  value: any\n): value is typeof ComponentWithRoles | ComponentWithRoles {\n  return typeof value?.getRole === 'function';\n}\n\n/**\n * Throws an error if the specified value is not a [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class) class or instance.\n *\n * @param value A value of any type.\n *\n * @category Utilities\n */\nexport function assertIsComponentWithRolesClassOrInstance(\n  value: any\n): asserts value is typeof ComponentWithRoles | ComponentWithRoles {\n  if (!isComponentWithRolesClassOrInstance(value)) {\n    throw new Error(\n      `Expected a component with roles class or instance, but received a value of type '${getTypeOf(\n        value\n      )}'`\n    );\n  }\n}\n"
  },
  {
    "path": "packages/with-roles/src/with-roles.test.ts",
    "content": "import {Component} from '@layr/component';\n\nimport {WithRoles} from './with-roles';\n\ndescribe('WithRoles', () => {\n  test('normalizePropertyOperationSetting()', async () => {\n    class Movie extends WithRoles(Component) {}\n\n    expect(Movie.normalizePropertyOperationSetting(true)).toBe(true);\n    expect(Movie.normalizePropertyOperationSetting('admin')).toEqual(['admin']);\n    expect(Movie.normalizePropertyOperationSetting(['user', 'guest'])).toEqual(['user', 'guest']);\n    expect(Movie.normalizePropertyOperationSetting([])).toBeUndefined();\n    expect(Movie.normalizePropertyOperationSetting([''])).toBeUndefined();\n\n    // @ts-expect-error\n    expect(() => Movie.normalizePropertyOperationSetting(undefined)).toThrow(\n      'The specified property operation setting (undefined) is invalid'\n    );\n    expect(() => Movie.normalizePropertyOperationSetting(false)).toThrow(\n      'The specified property operation setting (false) is invalid'\n    );\n    // @ts-expect-error\n    expect(() => Movie.normalizePropertyOperationSetting(1)).toThrow(\n      'The specified property operation setting (1) is invalid'\n    );\n    // @ts-expect-error\n    expect(() => Movie.normalizePropertyOperationSetting({admin: true})).toThrow(\n      'The specified property operation setting ({\"admin\":true}) is invalid'\n    );\n    // @ts-expect-error\n    expect(() => Movie.normalizePropertyOperationSetting([false])).toThrow(\n      'The specified property operation setting ([false]) is invalid'\n    );\n\n    expect(\n      // @ts-expect-error\n      Movie.normalizePropertyOperationSetting(undefined, {throwIfInvalid: false})\n    ).toBeUndefined();\n  });\n\n  test('resolvePropertyOperationSetting()', async () => {\n    class Movie extends WithRoles(Component) {}\n\n    expect(Movie.resolvePropertyOperationSetting(true)).toBe(true);\n    expect(Movie.prototype.resolvePropertyOperationSetting(true)).toBe(true);\n\n    let user: any;\n\n    const resolver = async () => user !== undefined;\n\n    Movie.setRole('user', resolver);\n\n    expect(await Movie.resolvePropertyOperationSetting(['user'])).toBe(false);\n\n    Movie.setRole('user', resolver); // Reset the role cache\n\n    user = {id: 'abc123'};\n\n    expect(await Movie.resolvePropertyOperationSetting(['user'])).toBe(true);\n  });\n\n  test('getRole() and hasRole()', async () => {\n    class Movie extends WithRoles(Component) {}\n\n    expect(() => Movie.getRole('anyone')).toThrow(\n      \"The role 'anyone' is missing (component: 'Movie')\"\n    );\n    expect(Movie.hasRole('anyone')).toBe(false);\n\n    const role = Movie.setRole('anyone', () => true);\n\n    expect(Movie.getRole('anyone')).toBe(role);\n    expect(Movie.prototype.hasRole('anyone')).toBe(false);\n    expect(Movie.prototype.getRole('anyone', {fallbackToClass: true})).toBe(role);\n\n    const MovieFork = Movie.fork();\n\n    const roleFork = MovieFork.getRole('anyone');\n\n    expect(roleFork).not.toBe(role);\n    expect(roleFork.isForkOf(role)).toBe(true);\n  });\n\n  test('setRole()', async () => {\n    class Movie extends WithRoles(Component) {}\n\n    const authorRole = Movie.prototype.setRole('author', () => true);\n\n    expect(Movie.prototype.getRole('author')).toBe(authorRole);\n    expect(Movie.hasRole('author')).toBe(false);\n\n    class Film extends Movie {}\n\n    expect(Film.prototype.getRole('author').isForkOf(authorRole)).toBe(true);\n\n    const adminRole = Film.prototype.setRole('admin', () => true);\n\n    expect(Film.prototype.getRole('admin')).toBe(adminRole);\n    expect(Movie.prototype.hasRole('admin')).toBe(false);\n  });\n\n  test('resolveRole()', async () => {\n    class Movie extends WithRoles(Component) {}\n\n    Movie.setRole('anyone', () => true);\n    Movie.setRole('admin', () => false);\n\n    expect(Movie.resolveRole('anyone')).toBe(true);\n    expect(Movie.resolveRole('admin')).toBe(false);\n    expect(Movie.prototype.resolveRole('anyone')).toBe(true);\n    expect(Movie.prototype.resolveRole('admin')).toBe(false);\n  });\n\n  test('getRoles()', async () => {\n    class Movie extends WithRoles(Component) {}\n\n    expect(Array.from(Movie.getRoles())).toEqual([]);\n\n    const anyoneRole = Movie.setRole('anyone', () => true);\n\n    expect(Array.from(Movie.getRoles())).toEqual([anyoneRole]);\n\n    const adminRole = Movie.setRole('admin', () => false);\n\n    expect(Array.from(Movie.getRoles())).toEqual([anyoneRole, adminRole]);\n  });\n});\n"
  },
  {
    "path": "packages/with-roles/src/with-roles.ts",
    "content": "import {\n  Component,\n  isComponentClass,\n  isComponentInstance,\n  PropertyOperationSetting\n} from '@layr/component';\nimport {possiblyAsync} from 'possibly-async';\nimport {hasOwnProperty, getTypeOf, Constructor, PromiseLikeable} from 'core-helpers';\n\nimport {Role, RoleResolver} from './role';\n\n/**\n * Extends a [`Component`](https://layrjs.com/docs/v2/reference/component) class with the ability to handle [roles](https://layrjs.com/docs/v2/reference/role).\n *\n * #### Usage\n *\n * Call `WithRoles()` with a [`Component`](https://layrjs.com/docs/v2/reference/component) class to construct a [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class) class. Next, use the [`@role()`](https://layrjs.com/docs/v2/reference/with-roles#role-decorator) decorator to define some [roles](https://layrjs.com/docs/v2/reference/role). Lastly, use the [`@expose()`](https://layrjs.com/docs/v2/reference/component#expose-decorator) decorator to authorize some attributes or methods according to these roles.\n *\n * **Example:**\n *\n * ```\n * import {Component, attribute, method, expose} from '@layr/component';\n * import {WithRoles, role} from '@layr/with-roles';\n *\n * class Article extends WithRoles(Component) {\n *   ﹫role('admin') static adminRoleResolver() {\n *     // ...\n *   }\n *\n *   // Only readable by an administrator\n *   ﹫expose({get: 'admin'}) ﹫attribute('number') viewCount = 0;\n *\n *   // Only callable by an administrator\n *   ﹫expose({call: 'admin'}) ﹫method() unpublish() {\n *     // ...\n *   }\n * }\n * ```\n *\n * Once you have a [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class), you can use any method provided by the `WithRoles()` mixin. For example, you can use the [`resolveRole()`](https://layrjs.com/docs/v2/reference/with-roles#resolve-role-dual-method) method to determine if the user has a specific role:\n *\n * ```\n * Article.resolveRole('admin'); // `true` or `false` depending on the current user\n * ```\n *\n * See the [\"Handling Authorization\"](https://layrjs.com/docs/v2/introduction/handling-authorization) tutorial for a comprehensive example using the `WithRoles()` mixin.\n *\n * ### ComponentWithRoles <badge type=\"primary\">class</badge> {#component-with-roles-class}\n *\n * *Inherits from [`Component`](https://layrjs.com/docs/v2/reference/component).*\n *\n * A `ComponentWithRoles` class is constructed by calling the `WithRoles()` mixin ([see above](https://layrjs.com/docs/v2/reference/with-roles#with-roles-mixin)).\n *\n * @mixin\n */\nexport function WithRoles<T extends Constructor<typeof Component>>(Base: T) {\n  if (!isComponentClass(Base)) {\n    throw new Error(\n      `The WithRoles mixin should be applied on a component class (received type: '${getTypeOf(\n        Base\n      )}')`\n    );\n  }\n\n  if (typeof (Base as any).getRole === 'function') {\n    return Base as T & typeof WithRoles;\n  }\n\n  class WithRoles extends Base {\n    static normalizePropertyOperationSetting(\n      setting: PropertyOperationSetting,\n      options: {throwIfInvalid?: boolean} = {}\n    ): PropertyOperationSetting | undefined {\n      const {throwIfInvalid = true} = options;\n\n      let normalizedSetting = super.normalizePropertyOperationSetting(setting, {\n        throwIfInvalid: false\n      });\n\n      if (normalizedSetting !== undefined) {\n        return normalizedSetting;\n      }\n\n      normalizedSetting = setting;\n\n      if (typeof normalizedSetting === 'string') {\n        normalizedSetting = [normalizedSetting];\n      }\n\n      if (\n        Array.isArray(normalizedSetting) &&\n        normalizedSetting.every((item) => typeof item === 'string')\n      ) {\n        normalizedSetting = normalizedSetting.filter((item) => item !== '');\n\n        if (normalizedSetting.length === 0) {\n          return undefined;\n        }\n\n        return normalizedSetting;\n      }\n\n      if (throwIfInvalid) {\n        throw new Error(\n          `The specified property operation setting (${JSON.stringify(setting)}) is invalid`\n        );\n      }\n\n      return undefined;\n    }\n\n    resolvePropertyOperationSetting(\n      setting: PropertyOperationSetting\n    ): PromiseLikeable<boolean | undefined> {\n      const resolvedSetting = super.resolvePropertyOperationSetting(setting);\n\n      if (resolvedSetting !== undefined) {\n        return resolvedSetting;\n      }\n\n      const roles = setting as string[];\n\n      return possiblyAsync.some(roles, (role) => this.resolveRole(role));\n    }\n\n    // === Roles ===\n\n    /**\n     * Gets a role. If there is no role with the specified name, an error is thrown.\n     *\n     * @param name The name of the role to get.\n     * @param [options.fallbackToClass] A boolean specifying whether the role should be get from the component class if there is no role with the specified name in the component prototype or instance (default: `false`).\n     *\n     * @returns A [Role](https://layrjs.com/docs/v2/reference/role) instance.\n     *\n     * @example\n     * ```\n     * Article.getRole('admin'); // => 'admin' role\n     *\n     * const article = new Article();\n     * article.getRole('admin'); // Error\n     * article.getRole('admin', {fallbackToClass: true}); // => 'admin' role\n     * ```\n     *\n     * @category Roles\n     */\n    static get getRole() {\n      return this.prototype.getRole;\n    }\n\n    /**\n     * Gets a role. If there is no role with the specified name, an error is thrown.\n     *\n     * @param name The name of the role to get.\n     * @param [options.fallbackToClass] A boolean specifying whether the role should be get from the component class if there is no role with the specified name in the component prototype or instance (default: `false`).\n     *\n     * @returns A [Role](https://layrjs.com/docs/v2/reference/role) instance.\n     *\n     * @example\n     * ```\n     * Article.getRole('admin'); // => 'admin' role\n     *\n     * const article = new Article();\n     * article.getRole('admin'); // Error\n     * article.getRole('admin', {fallbackToClass: true}); // => 'admin' role\n     * ```\n     *\n     * @category Roles\n     */\n    getRole(name: string, options: {fallbackToClass?: boolean} = {}) {\n      const {fallbackToClass = false} = options;\n\n      const role = this.__getRole(name, {fallbackToClass});\n\n      if (role === undefined) {\n        throw new Error(`The role '${name}' is missing (${this.describeComponent()})`);\n      }\n\n      return role;\n    }\n\n    /**\n     * Returns whether the [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class) has a role with the specified name.\n     *\n     * @param name The name of the role to check.\n     * @param [options.fallbackToClass] A boolean specifying whether the component class should be considered if there is no role with the specified name in the component prototype or instance (default: `false`).\n     *\n     * @returns A boolean.\n     *\n     * @example\n     * ```\n     * Article.hasRole('admin'); // => true\n     *\n     * const article = new Article();\n     * article.hasRole('admin'); // => false\n     * article.hasRole('admin', {fallbackToClass: true}); // => true\n     * ```\n     *\n     * @category Roles\n     */\n    static get hasRole() {\n      return this.prototype.hasRole;\n    }\n\n    /**\n     * Returns whether the [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class) has a role with the specified name.\n     *\n     * @param name The name of the role to check.\n     * @param [options.fallbackToClass] A boolean specifying whether the component class should be considered if there is no role with the specified name in the component prototype or instance (default: `false`).\n     *\n     * @returns A boolean.\n     *\n     * @example\n     * ```\n     * Article.hasRole('admin'); // => true\n     *\n     * const article = new Article();\n     * article.hasRole('admin'); // => false\n     * article.hasRole('admin', {fallbackToClass: true}); // => true\n     * ```\n     *\n     * @category Roles\n     */\n    hasRole(name: string, options: {fallbackToClass?: boolean} = {}) {\n      const {fallbackToClass = false} = options;\n\n      return this.__getRole(name, {fallbackToClass}) !== undefined;\n    }\n\n    static get __getRole() {\n      return this.prototype.__getRole;\n    }\n\n    __getRole(name: string, {fallbackToClass}: {fallbackToClass: boolean}): Role | undefined {\n      const roles = this.__getRoles();\n\n      let role = roles[name];\n\n      if (role !== undefined) {\n        if (!hasOwnProperty(roles, name)) {\n          role = role.fork(this);\n          roles[name] = role;\n        }\n\n        return role;\n      }\n\n      if (isComponentInstance(this) && fallbackToClass) {\n        return this.__getRole.call(this.constructor, name, {fallbackToClass});\n      }\n\n      return undefined;\n    }\n\n    /**\n     * Sets a role in the current [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class) class or prototype.\n     *\n     * Typically, instead of using this method, you would rather use the [`@role()`](https://layrjs.com/docs/v2/reference/with-roles#role-decorator) decorator.\n     *\n     * @param name The name of the role.\n     * @param resolver A function that should return a boolean indicating whether a user has the corresponding role. The function can be asynchronous and is called with the role's parent as `this` context.\n     *\n     * @returns The [Role](https://layrjs.com/docs/v2/reference/role) instance that was created.\n     *\n     * @example\n     * ```\n     * Article.setRole('admin', function () {\n     *  // ...\n     * });\n     * ```\n     *\n     * @category Roles\n     */\n    static get setRole() {\n      return this.prototype.setRole;\n    }\n\n    /**\n     * Sets a role in the current [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class) class or prototype.\n     *\n     * Typically, instead of using this method, you would rather use the [`@role()`](https://layrjs.com/docs/v2/reference/with-roles#role-decorator) decorator.\n     *\n     * @param name The name of the role.\n     * @param resolver A function that should return a boolean indicating whether a user has the corresponding role. The function can be asynchronous and is called with the role's parent as `this` context.\n     *\n     * @returns The [Role](https://layrjs.com/docs/v2/reference/role) instance that was created.\n     *\n     * @example\n     * ```\n     * Article.setRole('admin', function () {\n     *  // ...\n     * });\n     * ```\n     *\n     * @category Roles\n     */\n    setRole(name: string, resolver: RoleResolver): Role {\n      const roles = this.__getRoles();\n\n      const role = new Role(name, this, resolver);\n\n      roles[name] = role;\n\n      return role;\n    }\n\n    /**\n     * Resolves a role by calling its resolver function. If there is no role with the specified name in the [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class) class or prototype, an error is thrown.\n     *\n     * The resolver function is called with the role's parent as `this` context.\n     *\n     * Once a role has been resolved, the result is cached, so the resolver function is called only one time.\n     *\n     * @param name The name of the role to resolve.\n     *\n     * @returns A boolean.\n     *\n     * @example\n     * ```\n     * Article.resolveRole('admin'); // `true` or `false` depending on the current user\n     * ```\n     *\n     * @category Roles\n     * @possiblyasync\n     */\n    static get resolveRole() {\n      return this.prototype.resolveRole;\n    }\n\n    /**\n     * Resolves a role by calling its resolver function. If there is no role with the specified name in the [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class) class or prototype, an error is thrown.\n     *\n     * The resolver function is called with the role's parent as `this` context.\n     *\n     * Once a role has been resolved, the result is cached, so the resolver function is called only one time.\n     *\n     * @param name The name of the role to resolve.\n     *\n     * @returns A boolean.\n     *\n     * @example\n     * ```\n     * Article.resolveRole('admin'); // `true` or `false` depending on the current user\n     * ```\n     *\n     * @category Roles\n     * @possiblyasync\n     */\n    resolveRole(name: string) {\n      return this.getRole(name, {fallbackToClass: true}).resolve();\n    }\n\n    static get getRoles() {\n      return this.prototype.getRoles;\n    }\n\n    getRoles() {\n      const withRoles = this;\n\n      return {\n        *[Symbol.iterator]() {\n          for (const name in withRoles.__getRoles()) {\n            yield withRoles.getRole(name);\n          }\n        }\n      };\n    }\n\n    static __roles: {[name: string]: Role};\n\n    static get __getRoles() {\n      return this.prototype.__getRoles;\n    }\n\n    __roles!: {[name: string]: Role};\n\n    __getRoles() {\n      if (this.__roles === undefined) {\n        this.__roles = Object.create(null);\n      } else if (!hasOwnProperty(this, '__roles')) {\n        this.__roles = Object.create(this.__roles);\n      }\n\n      return this.__roles;\n    }\n  }\n\n  return WithRoles;\n}\n\nexport class ComponentWithRoles extends WithRoles(Component) {}\n"
  },
  {
    "path": "packages/with-roles/tsconfig.json",
    "content": "{\n  \"extends\": \"@mvila/tsconfig\",\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "website/README.md",
    "content": "# Layr Website\n\nThis directory contains the source code of the [Layr](https://layrjs.com) website.\n\n## About\n\nThe Layr website is a single-page application created with [Layr](https://github.com/layrjs/layr). The frontend is statically hosted in AWS S3 + CloudFront and the backend is serverlessly hosted in AWS Lambda + API Gateway. Regarding the database, it is a free-tier MongoDB Atlas cluster with a daily backup that is handled by a Lambda function.\n\n## Prerequisites\n\n- Make sure your have a [Node.js](https://nodejs.org/) (v14 or newer) installed.\n- Make sure you have [Boostr](https://boostr.dev/) installed as it is used to manage the development environment.\n\n## Installation\n\nInstall all the npm dependencies with the following command:\n\n```sh\nboostr install\n```\n\n## Development\n\n### Configuration\n\n- Generate a JWT secret by running the following command in your terminal:\n  - `openssl rand -hex 64`\n- In the `backend` directory, duplicate the `boostr.config.private-template.mjs` file, name it `boostr.config.private.mjs`, and modify it to set all the required private development environment variables.\n\n### Migrating the database\n\nMigrate the database with the following command:\n\n```sh\nboostr database migrate\n```\n\n### Starting the development environment\n\nStart the development environment with the following command:\n\n```\nboostr start\n```\n\nThe website should be available at http://localhost:18887.\n\n## Production\n\n### Configuration\n\n- Configure a [MailerLite](https://www.mailerlite.com/) domain and visit https://app.mailerlite.com/integrations/api/ to get your API key and your subscriber group ID.\n- Generate a JWT secret by running the following command in your terminal:\n  - `openssl rand -hex 64`\n- In the `backend` directory, duplicate the `boostr.config.private-template.mjs` file, name it `boostr.config.private.mjs`, and modify it to set all the required private production environment variables.\n- In the `database` directory, duplicate the `boostr.config.private-template.mjs` file, name it `boostr.config.private.mjs`, and modify it to set the `stages.production.url` attribute to the URL of your production MongoDB database.\n\n### Migrating the database\n\nMigrate the database with the following command:\n\n```sh\nboostr database migrate --production\n```\n\n### Deployment\n\nDeploy the website to production with the following command:\n\n```\nboostr deploy --production\n```\n\nThe website should be available at https://layrjs.com.\n"
  },
  {
    "path": "website/backend/boostr.config.mjs",
    "content": "export default ({services}) => ({\n  type: 'backend',\n\n  dependsOn: 'database',\n\n  environment: {\n    FRONTEND_URL: services.frontend.url,\n    BACKEND_URL: services.backend.url,\n    DATABASE_URL: services.database.url\n  },\n\n  rootComponent: './src/index.ts',\n\n  stages: {\n    development: {\n      url: 'http://localhost:18888/',\n      platform: 'local'\n    },\n    production: {\n      url: 'https://backend.layrjs.com/',\n      platform: 'aws',\n      aws: {\n        region: 'us-west-2',\n        lambda: {\n          memorySize: 1024,\n          timeout: 15\n        }\n      }\n    }\n  }\n});\n"
  },
  {
    "path": "website/backend/boostr.config.private-template.mjs",
    "content": "export default () => ({\n  stages: {\n    development: {\n      environment: {\n        JWT_SECRET: '********'\n      }\n    },\n    production: {\n      environment: {\n        MAILER_LITE_API_KEY: '********',\n        MAILER_LITE_NEWSLETTER_SUBSCRIPTIONS_GROUP_ID: '********',\n        JWT_SECRET: '********'\n      }\n    }\n  }\n});\n"
  },
  {
    "path": "website/backend/package.json",
    "content": "{\n  \"name\": \"layr-website-backend\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"@layr/component\": \"^2.0.31\",\n    \"@layr/mongodb-store\": \"^2.0.45\",\n    \"@layr/routable\": \"^2.0.84\",\n    \"@layr/storable\": \"^2.0.49\",\n    \"@layr/utilities\": \"^1.0.5\",\n    \"@layr/with-roles\": \"^2.0.36\",\n    \"bcryptjs\": \"^2.4.3\",\n    \"cross-fetch\": \"^3.1.4\",\n    \"jsonwebtoken\": \"^9.0.0\",\n    \"lodash\": \"^4.17.21\",\n    \"rss\": \"^1.2.2\",\n    \"slugify\": \"^1.6.1\"\n  },\n  \"devDependencies\": {\n    \"@boostr/tsconfig\": \"^1.0.0\",\n    \"@types/bcryptjs\": \"^2.4.2\",\n    \"@types/jsonwebtoken\": \"^8.5.9\",\n    \"@types/lodash\": \"^4.14.175\",\n    \"@types/node\": \"^14.17.20\",\n    \"@types/rss\": \"0.0.28\"\n  }\n}\n"
  },
  {
    "path": "website/backend/src/components/application.ts",
    "content": "import {Component, provide} from '@layr/component';\nimport {Routable} from '@layr/routable';\n\nimport {User} from './user';\nimport {Article} from './article';\nimport {Newsletter} from './newsletter';\n\nexport class Application extends Routable(Component) {\n  @provide() static User = User;\n  @provide() static Article = Article;\n  @provide() static Newsletter = Newsletter;\n}\n"
  },
  {
    "path": "website/backend/src/components/article.ts",
    "content": "import {expose, validators} from '@layr/component';\nimport {secondaryIdentifier, attribute, method} from '@layr/storable';\nimport {Routable, httpRoute} from '@layr/routable';\nimport slugify from 'slugify';\nimport RSS from 'rss';\nimport escape from 'lodash/escape';\n\nimport {Entity} from './entity';\nimport {WithAuthor} from './with-author';\n\nconst {rangeLength} = validators;\n\n@expose({\n  get: {call: true},\n  find: {call: true},\n  count: {call: true},\n  prototype: {\n    load: {call: true},\n    save: {call: 'author'},\n    delete: {call: 'author'}\n  }\n})\nexport class Article extends Routable(WithAuthor(Entity)) {\n  @expose({get: true, set: 'author'})\n  @attribute('string', {validators: [rangeLength([1, 200])]})\n  title = '';\n\n  @expose({get: true, set: 'author'})\n  @attribute('string', {validators: [rangeLength([1, 2000])]})\n  description = '';\n\n  @expose({get: true, set: 'author'})\n  @attribute('string', {validators: [rangeLength([1, 50000])]})\n  body = '';\n\n  @expose({get: true})\n  @secondaryIdentifier('string', {validators: [rangeLength([8, 300])]})\n  slug = this.generateSlug();\n\n  generateSlug() {\n    this.validate({title: true});\n\n    return (\n      slugify(this.title, {remove: /[^\\w\\s-]/g}) +\n      '-' +\n      ((Math.random() * Math.pow(36, 6)) | 0).toString(36)\n    );\n  }\n\n  // time curl -v http://localhost:18888/blog/feed\n  @httpRoute('GET', '/blog/feed', {\n    transformers: {\n      output(result) {\n        return {\n          status: 200,\n          headers: {'content-type': 'application/rss+xml'},\n          body: result\n        };\n      }\n    }\n  })\n  @expose({call: true})\n  @method()\n  static async getRSSFeed() {\n    const articles = await this.find(\n      {},\n      {\n        title: true,\n        description: true,\n        slug: true,\n        author: {fullName: true},\n        createdAt: true\n      },\n      {\n        sort: {createdAt: 'desc'},\n        limit: 10\n      }\n    );\n\n    const feed = new RSS({\n      title: 'Layr Blog',\n      description: 'Dramatically simplify full‑stack development',\n      feed_url: `${process.env.BACKEND_URL}blog/feed`,\n      site_url: `${process.env.FRONTEND_URL}blog`\n    });\n\n    for (const article of articles) {\n      const url = `${process.env.FRONTEND_URL}blog/articles/${article.slug}`;\n\n      const description = `<p>${escape(\n        article.description\n      )}</p>\\n<p><a href=\"${url}\">Read more...</a></p>`;\n\n      feed.item({\n        url,\n        title: article.title,\n        description,\n        author: article.author?.fullName,\n        date: article.createdAt,\n        guid: article.slug\n      });\n    }\n\n    const xml = feed.xml({indent: true});\n\n    return xml;\n  }\n}\n"
  },
  {
    "path": "website/backend/src/components/entity.ts",
    "content": "import {Component, consume, expose, AttributeSelector} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute, index} from '@layr/storable';\nimport {WithRoles, role} from '@layr/with-roles';\n\nimport type {User} from './user';\n\nexport class Entity extends WithRoles(Storable(Component)) {\n  @consume() static User: typeof User;\n\n  @expose({get: true, set: true}) @primaryIdentifier() id!: string;\n\n  @expose({get: true}) @index() @attribute('Date') createdAt = new Date();\n\n  @attribute('Date?') updatedAt?: Date;\n\n  @role('user') static async userRoleResolver() {\n    return (await this.User.getAuthenticatedUser()) !== undefined;\n  }\n\n  @role('guest') static async guestRoleResolver() {\n    return !(await this.resolveRole('user'));\n  }\n\n  async beforeSave(attributeSelector: AttributeSelector) {\n    await super.beforeSave(attributeSelector);\n\n    this.updatedAt = new Date();\n  }\n}\n"
  },
  {
    "path": "website/backend/src/components/newsletter.ts",
    "content": "import {Component, method, expose} from '@layr/component';\nimport fetch from 'cross-fetch';\n\nconst MAILER_LITE_API_URL = 'https://connect.mailerlite.com/api/';\n\nconst mailerLiteAPIKey = process.env.MAILER_LITE_API_KEY;\n\nconst mailerLiteNewsletterSubscriptionsGroupId =\n  process.env.MAILER_LITE_NEWSLETTER_SUBSCRIPTIONS_GROUP_ID;\n\nexport class Newsletter extends Component {\n  @expose({call: true}) @method() static async subscribe({email}: {email: string}) {\n    if (!(mailerLiteAPIKey && mailerLiteNewsletterSubscriptionsGroupId)) {\n      throw new Error('MailerLite configuration is missing');\n    }\n\n    const response = await fetch(`${MAILER_LITE_API_URL}subscribers`, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        'Accept': 'application/json',\n        'Authorization': `Bearer ${mailerLiteAPIKey}`,\n        'X-Version': '2023-10-02'\n      },\n      body: JSON.stringify({\n        email,\n        groups: [mailerLiteNewsletterSubscriptionsGroupId],\n        status: 'active'\n      })\n    });\n\n    if (!(response.status === 201 || response.status === 200)) {\n      throw new Error('An error occurred while adding a subscriber to MailerLite');\n    }\n\n    const result = await response.json();\n\n    if (!result?.data?.id) {\n      throw new Error('An error occurred while adding a subscriber to MailerLite');\n    }\n\n    console.log(`[NEWSLETTER] '${email}' subscribed`);\n  }\n}\n"
  },
  {
    "path": "website/backend/src/components/user.ts",
    "content": "import {expose, validators, AttributeSelector} from '@layr/component';\nimport {secondaryIdentifier, attribute, method, loader} from '@layr/storable';\nimport {role} from '@layr/with-roles';\nimport {throwError} from '@layr/utilities';\nimport bcrypt from 'bcryptjs';\nimport compact from 'lodash/compact';\n\nimport {Entity} from './entity';\nimport {generateJWT, verifyJWT} from '../jwt';\n\nconst TOKEN_DURATION = 31536000000; // 1 year\nconst INVITE_TOKEN_DURATION = 604800000; // 1 week\nconst BCRYPT_SALT_ROUNDS = 5;\n\nconst {rangeLength} = validators;\n\n@expose({get: {call: true}, prototype: {load: {call: true}, save: {call: 'self'}}})\nexport class User extends Entity {\n  declare ['constructor']: typeof User;\n\n  @expose({get: 'self', set: ['creator', 'self']})\n  @secondaryIdentifier('string', {\n    validators: [rangeLength([3, 100])],\n\n    async beforeSave(this: User) {\n      const User = this.constructor.fork().detach();\n\n      const existingUser = await User.get({email: this.email}, {}, {throwIfMissing: false});\n\n      if (existingUser !== undefined && existingUser.id !== this.id) {\n        throwError('Email already registered', {\n          displayMessage: 'This email address is already registered.'\n        });\n      }\n    }\n  })\n  email = '';\n\n  @expose({set: ['creator', 'self']})\n  @attribute('string', {\n    validators: [rangeLength([1, 100])],\n\n    async beforeSave(this: User) {\n      this.password = await this.constructor.hashPassword(this.password);\n    }\n  })\n  password = '';\n\n  @expose({get: true, set: ['creator', 'self']})\n  @attribute('string', {validators: [rangeLength([1, 32])]})\n  firstName = '';\n\n  @expose({get: true, set: ['creator', 'self']})\n  @attribute('string', {validators: [rangeLength([1, 32])]})\n  lastName = '';\n\n  @expose({get: true})\n  @loader(async function (this: User) {\n    const ghost = this.getGhost();\n\n    await ghost.load({firstName: true, lastName: true});\n\n    return compact([ghost.firstName, ghost.lastName]).join(' ');\n  })\n  @attribute('string')\n  fullName!: string;\n\n  @expose({get: true, set: ['creator', 'self']})\n  @attribute('string', {validators: [rangeLength([10, 128])]})\n  url = '';\n\n  @expose({get: true, set: true})\n  @attribute('string?')\n  static token?: string;\n\n  @role('creator') creatorRoleResolver() {\n    return this.isNew();\n  }\n\n  @role('self') async selfRoleResolver() {\n    if ((await this.resolveRole('creator')) || (await this.resolveRole('guest'))) {\n      return undefined;\n    }\n\n    return this === (await this.constructor.getAuthenticatedUser());\n  }\n\n  @expose({call: true}) @method() static async getAuthenticatedUser(\n    attributeSelector: AttributeSelector = {}\n  ) {\n    if (this.token === undefined) {\n      return;\n    }\n\n    const userId = this.verifyToken(this.token);\n\n    if (userId === undefined) {\n      // The token is invalid or expired\n      this.token = undefined;\n      return;\n    }\n\n    const user = await this.get(userId, attributeSelector, {throwIfMissing: false});\n\n    if (user === undefined) {\n      // The user doesn't exist anymore\n      this.token = undefined;\n      return;\n    }\n\n    return user;\n  }\n\n  static verifyToken(token: string) {\n    const payload = verifyJWT(token) as {sub: string} | undefined;\n    const userId = payload?.sub;\n\n    return userId;\n  }\n\n  static generateToken(userId: string, {expiresIn = TOKEN_DURATION} = {}) {\n    const token = generateJWT({\n      sub: userId,\n      exp: Math.round((Date.now() + expiresIn) / 1000)\n    });\n\n    return token;\n  }\n\n  @expose({call: 'creator'}) @method() async signUp({inviteToken}: {inviteToken?: string} = {}) {\n    const {User} = this.constructor;\n\n    const isAllowed = await (async () => {\n      if (inviteToken) {\n        return User.verifyInviteToken(inviteToken);\n      }\n\n      const numberOfUsers = await User.count();\n      return numberOfUsers === 0;\n    })();\n\n    if (!isAllowed) {\n      throwError('Signing up is not allowed', {\n        displayMessage: 'Signing up requires a valid invite token.'\n      });\n    }\n\n    await this.save();\n\n    User.token = User.generateToken(this.id);\n  }\n\n  @expose({call: 'creator'}) @method() async signIn() {\n    const {User} = this.constructor;\n\n    this.validate({email: true, password: true});\n\n    const existingUser = await User.fork()\n      .detach()\n      .get({email: this.email}, {password: true}, {throwIfMissing: false});\n\n    if (existingUser === undefined) {\n      throwError('User not found', {\n        displayMessage: 'There is no user registered with that email address.'\n      });\n    }\n\n    if (!(await this.verifyPassword(existingUser))) {\n      throwError('Wrong password', {displayMessage: 'The password you entered is incorrect.'});\n    }\n\n    User.token = User.generateToken(existingUser.id);\n  }\n\n  static async hashPassword(password: string) {\n    return await bcrypt.hash(password, BCRYPT_SALT_ROUNDS);\n  }\n\n  async verifyPassword(existingUser: User) {\n    return await bcrypt.compare(this.password, existingUser.password);\n  }\n\n  @expose({call: 'user'}) @method() static generateInviteToken({\n    expiresIn = INVITE_TOKEN_DURATION\n  } = {}) {\n    return generateJWT({\n      isInvited: true,\n      exp: Math.round((Date.now() + expiresIn) / 1000)\n    });\n  }\n\n  static verifyInviteToken(token: string) {\n    const payload = verifyJWT(token) as {isInvited: boolean} | undefined;\n    return payload?.isInvited === true;\n  }\n}\n"
  },
  {
    "path": "website/backend/src/components/with-author.ts",
    "content": "import {expose} from '@layr/component';\nimport {attribute} from '@layr/storable';\nimport {role} from '@layr/with-roles';\n\nimport type {Entity} from './entity';\nimport type {User} from './user';\n\nexport const WithAuthor = (Base: typeof Entity) => {\n  class WithAuthor extends Base {\n    declare ['constructor']: typeof WithAuthor;\n\n    @expose({get: true, set: 'author'}) @attribute('User') author!: User;\n\n    @role('author') async authorRoleResolver() {\n      if (await this.resolveRole('guest')) {\n        return undefined;\n      }\n\n      if (this.isNew()) {\n        return true;\n      }\n\n      await this.getGhost().load({author: {}});\n\n      return (\n        this.getGhost().author === (await this.constructor.User.getAuthenticatedUser())!.getGhost()\n      );\n    }\n  }\n\n  return WithAuthor;\n};\n"
  },
  {
    "path": "website/backend/src/index.ts",
    "content": "import {MongoDBStore} from '@layr/mongodb-store';\n\nimport {Application} from './components/application';\n\nexport default () => {\n  const store = new MongoDBStore(process.env.DATABASE_URL!);\n\n  store.registerRootComponent(Application);\n\n  return Application;\n};\n"
  },
  {
    "path": "website/backend/src/jwt.ts",
    "content": "import jwt from 'jsonwebtoken';\n\n// Tip: Use `openssl rand -hex 64` to generate a JWT secret\n\nconst secretBuffer = Buffer.from(process.env.JWT_SECRET!, 'hex');\nconst algorithm = 'HS256';\n\nexport function generateJWT(payload: object) {\n  return jwt.sign(payload, secretBuffer, {algorithm});\n}\n\nexport function verifyJWT(token: string) {\n  try {\n    return jwt.verify(token, secretBuffer, {algorithms: [algorithm]});\n  } catch (err) {\n    return undefined;\n  }\n}\n"
  },
  {
    "path": "website/backend/tsconfig.json",
    "content": "{\n  \"extends\": \"@boostr/tsconfig\",\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "website/boostr.config.mjs",
    "content": "export default () => ({\n  type: 'application',\n\n  services: {\n    frontend: './frontend',\n    backend: './backend',\n    database: './database'\n  },\n\n  environment: {\n    APPLICATION_NAME: 'Layr',\n    APPLICATION_DESCRIPTION: 'Dramatically simplify full‑stack development.'\n  },\n\n  stages: {\n    production: {\n      environment: {\n        NODE_ENV: 'production'\n      }\n    }\n  }\n});\n"
  },
  {
    "path": "website/database/.gitignore",
    "content": "/data\n"
  },
  {
    "path": "website/database/boostr.config.mjs",
    "content": "export default () => ({\n  type: 'database',\n\n  stages: {\n    development: {\n      url: 'mongodb://localhost:18889/dev',\n      platform: 'local'\n    }\n  }\n});\n"
  },
  {
    "path": "website/database/boostr.config.private-template.mjs",
    "content": "export default () => ({\n  stages: {\n    production: {\n      url: '********'\n    }\n  }\n});\n"
  },
  {
    "path": "website/frontend/boostr.config.mjs",
    "content": "export default ({services}) => ({\n  type: 'web-frontend',\n\n  dependsOn: 'backend',\n\n  environment: {\n    BACKEND_URL: services.backend.url\n  },\n\n  rootComponent: './src/index.ts',\n\n  html: {\n    language: 'en',\n    head: {\n      title: services.frontend.environment.APPLICATION_NAME,\n      metas: [\n        {name: 'description', content: services.frontend.environment.APPLICATION_DESCRIPTION},\n        {charset: 'utf-8'},\n        {name: 'viewport', content: 'width=device-width, initial-scale=1'},\n        {'http-equiv': 'x-ua-compatible', 'content': 'ie=edge'}\n      ],\n      links: [\n        {rel: 'icon', href: '/layr-favicon-3vtu1VGUfUfDawVC0zL4Oz.immutable.png'},\n        {\n          rel: 'alternate',\n          type: 'application/rss+xml',\n          title: 'Layr Blog Feed',\n          href: `${services.backend.url}blog/feed`\n        },\n        {\n          rel: 'stylesheet',\n          href: 'https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&display=swap'\n        }\n      ],\n      style: `\n        /* Set body colors ASAP to avoid a white page */\n        body {\n          color: #eceff1;\n          background-color: #263238;\n        }\n        /* Avoid an Emotion warning */\n        h1:first-child,\n        h2:first-child,\n        h3:first-child,\n        h4:first-child,\n        h5:first-child,\n        h6:first-child {\n          margin-top: 0 !important;\n        }\n      `,\n      scripts: [\n        {\n          'async': true,\n          'defer': true,\n          'data-domain': 'layrjs.com',\n          'src': 'https://plausible.io/js/plausible.js'\n        }\n      ]\n    }\n  },\n\n  stages: {\n    development: {\n      url: 'http://localhost:18887/',\n      platform: 'local'\n    },\n    production: {\n      url: 'https://layrjs.com/',\n      platform: 'aws',\n      aws: {\n        region: 'us-west-2',\n        cloudFront: {\n          priceClass: 'PriceClass_100'\n        }\n      }\n    }\n  }\n});\n"
  },
  {
    "path": "website/frontend/package.json",
    "content": "{\n  \"name\": \"layr-website-frontend\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"@emotion-kit/react\": \"^1.0.59\",\n    \"@emotion-starter/react\": \"^1.0.70\",\n    \"@emotion/react\": \"^11.4.1\",\n    \"@layr/component\": \"^2.0.31\",\n    \"@layr/component-http-client\": \"^2.0.44\",\n    \"@layr/react-integration\": \"^2.0.109\",\n    \"@layr/routable\": \"^2.0.84\",\n    \"@layr/storable\": \"^2.0.49\",\n    \"@layr/utilities\": \"^1.0.5\",\n    \"@react-hook/window-size\": \"^3.0.7\",\n    \"cross-fetch\": \"^3.1.4\",\n    \"date-fns\": \"^2.24.0\",\n    \"dompurify\": \"^2.3.3\",\n    \"highlight.js\": \"^10.7.3\",\n    \"lodash\": \"^4.17.21\",\n    \"marked\": \"^4.0.12\",\n    \"react\": \"^17.0.1\",\n    \"react-dom\": \"^17.0.1\",\n    \"react-helmet\": \"^6.1.0\"\n  },\n  \"devDependencies\": {\n    \"@boostr/tsconfig\": \"^1.0.0\",\n    \"@types/dompurify\": \"^2.3.1\",\n    \"@types/lodash\": \"^4.14.175\",\n    \"@types/marked\": \"^4.0.1\",\n    \"@types/react\": \"^17.0.27\",\n    \"@types/react-dom\": \"^17.0.9\",\n    \"@types/react-helmet\": \"^6.1.3\"\n  }\n}\n"
  },
  {
    "path": "website/frontend/public/docs/v1/introduction/authorization-6eooNLZnaLZk4eJNHNGwUp-edited.immutable.md",
    "content": "### Handling Authorization\n\nIn the [previous guide](https://layrjs.com/docs/v1/introduction/routing), we added a feature to our \"Guestbook\" application so that the user can edit an existing message. We have a problem though. Currently, anyone can edit any message of the guestbook, and that's certainly not what we want. The ability to edit any message should be restricted to the administrator of the guestbook. So, how can we implement this restriction?\n\n> TLDR: The completed project is available in the <!-- <if language=\"js\"> -->[Layr repository](https://github.com/layrjs/layr/tree/master/examples/v1/guestbook-web-with-authorization-js)<!-- </if> --><!-- <if language=\"ts\"> -->[Layr repository](https://github.com/layrjs/layr/tree/master/examples/v1/guestbook-web-with-authorization-ts)<!-- </if> -->.\n\n#### Preparing the Project\n\nYou can duplicate the [previous project](https://layrjs.com/docs/v1/introduction/routing) or simply modify it in place.\n\n#### Modifying the Backend\n\nTo restrict the use of a particular feature to a user, we need to be able to:\n\n1. Authenticate the user.\n2. Authorize the authenticated user to use a particular feature.\n\nTo keep this guide simple, we're going to fully implement only the authorization part of the mechanism. For the authentication part, we'll \"cheat\" a bit by implementing a minimal authentication system based on a secret shared between the frontend and the backend.\n\n> For an example of a full authentication system with user registration and password verification, you can have a look at the Layr's [RealWorld example](https://github.com/layrjs/react-layr-realworld-example-app) implementation.\n\n##### Authenticating the User\n\nLet's start by implementing the authentication system.\n\nIn the <!-- <if language=\"js\"> -->`src/backend.js`<!-- </if> --><!-- <if language=\"ts\"> -->`src/backend.ts`<!-- </if> --> file, define a new `Session` component just before the `Message` component:\n\n```js\n// JS\n\nexport class Session extends Component {\n  @expose({set: true})\n  @attribute('string?')\n  static secret;\n\n  static isAdmin() {\n    return this.secret === process.env.ADMIN_SECRET;\n  }\n}\n```\n\n```ts\n// TS\n\nexport class Session extends Component {\n  @expose({set: true})\n  @attribute('string?')\n  static secret?: string;\n\n  static isAdmin() {\n    return this.secret === process.env.ADMIN_SECRET;\n  }\n}\n```\n\nThis component will be in charge of handling a user's session and is made of:\n\n- A `secret` attribute that can be set from the frontend thanks to the [`@expose()`](https://layrjs.com/docs/v1/reference/component#expose-decorator) decorator.\n- An `isAdmin()` method that compares the value of the `secret` attribute with the value of an `ADMIN_SECRET` environment variable which is available only in the backend.\n\nVoilà! We've implemented our minimal authentication system in the backend.\n\n##### Authorizing the User\n\nCurrently, we already have some sort of authorizations in the backend, but they are pretty binary. We've used the [`@expose()`](https://layrjs.com/docs/v1/reference/component#expose-decorator) decorator to expose some methods to the frontend:\n\n```js\n@expose({\n  find: {call: true},\n  prototype: {\n    load: {call: true},\n    save: {call: true}\n  }\n})\n```\n\nBy specifying `{call: true}`, we've indicated that the `call` operation of a certain method is always authorized. So, for example, the `save()` method can be called by anyone from the frontend. But that's not what we want. We'd like the `save()` method to be callable only by the creator of a new message or by an administrator.\n\nSo, first, modify the class exposure as follows:\n\n```js\n@expose({\n  find: {call: true},\n  prototype: {\n    load: {call: true},\n    save: {call: ['creator', 'admin']}\n  }\n})\n```\n\nSo now the `save()` method is callable only by a `'creator'` (of a new message) or an `'admin'`. The strings `'creator'` and `'admin'` represent the names of some roles that we're going to define.\n\nFirst, install the `@layr/with-roles` package:\n\n```sh\nnpm install @layr/with-roles@1\n```\n\nNext, add the following line at the beginning of the <!-- <if language=\"js\"> -->`src/backend.js`<!-- </if> --><!-- <if language=\"ts\"> -->`src/backend.ts`<!-- </if> --> file to import the [`WithRoles()`](https://layrjs.com/docs/v1/reference/with-roles) mixin and the [`@role()`](https://layrjs.com/docs/v1/reference/with-roles#role-decorator) decorator:\n\n```js\nimport {WithRoles, role} from '@layr/with-roles';\n```\n\nNext, change the `Message` class definition as follows:\n\n```js\nexport class Message extends WithRoles(Storable(Component)) {\n  // ...\n}\n```\n\nBy using the [`WithRoles()`](https://layrjs.com/docs/v1/reference/with-roles) mixin, we've given the `Message` class the ability to handle roles.\n\nNext, add the following lines inside the `Message` class to define the roles that we need:\n\n```js\n@role('creator') creatorRoleResolver() {\n  return this.isNew();\n}\n\n@role('admin') static adminRoleResolver() {\n  return this.Session.isAdmin();\n}\n```\n\nA [role](https://layrjs.com/docs/v1/reference/role) is defined by a name and a method that should return a boolean indicating whether the user has a specific role.\n\nThe `creatorRoleResolver()` method simply returns the result of the [`isNew()`](https://layrjs.com/docs/v1/reference/component#is-new-instance-method) method. So when a message is new, the user gets the `creator` role.\n\nThe `adminRoleResolver()` method is calling the `Session.isAdmin()` method to determine whether the user as the `'admin'` role. Notice that since the `adminRoleResolver()` method doesn't depend on a particular message, it can be defined as a class method (with the `static` keyword).\n\nNext, add the following line inside the `Message` class (preferably at the beginning) to make the `Message` component aware of the `Session` component:\n\n```js\n@provide() static Session = Session;\n```\n\nLastly, since we are using the [`@provide()`](https://layrjs.com/docs/v1/reference/component#provide-decorator) decorator, we need to import it by modifying the first line of the file as follows:\n\n```js\nimport {Component, provide, expose, validators} from '@layr/component';\n```\n\n#### Testing the Application\n\nStart the backend with the following command:\n\n```sh\n// JS\n\nADMIN_SECRET=xyz123 npx babel-node ./src/backend.js\n```\n\n```sh\n// TS\n\nADMIN_SECRET=xyz123 npx ts-node ./src/backend.ts\n```\n\nNotice how we specified the `ADMIN_SECRET` environment variable so the backend can get it from `process.env.ADMIN_SECRET`.\n\nNext, start the frontend in another terminal:\n\n```sh\nnpx webpack serve --mode=development\n```\n\nLastly, open [http://localhost:8080/](http://localhost:8080/) in a browser.\n\nYou should see the same thing as [before](https://layrjs.com/docs/v1/introduction/routing#testing-the-application), except that when you try to submit an edited message, you should see the following message:\n\n```json\nSorry, an error occurred while submitting your message.\n```\n\nAnd if you open the browser's console, you should see the following error:\n\n```json\nError: Cannot execute a method that is not allowed (name: 'save')\n```\n\nThat's all good! It means that the backend is now protected against unauthorized message modifications.\n\n#### Modifying the Frontend\n\nWe want to allow the administrator to edit some messages, and to do this we need to find a way to send the administrator's secret to the backend.\n\nIt's actually quite easy. All we need to do is to extend the `Session` component in the frontend so the value of the `secret` attribute can be set by the user. We could implement a user interface for that, but once again, we'll make it as simple as possible. So we'll read the secret from the browser's [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) and we'll let the administrator set its secret manually by using the browser's developer tools.\n\nIn the <!-- <if language=\"js\"> -->`src/frontend.js`<!-- </if> --><!-- <if language=\"ts\"> -->`src/frontend.tsx`<!-- </if> --> file, add the following code just before the `Message` component:\n\n```js\n// JS\n\nclass Session extends BackendMessage.Session {\n  @attribute('string?', {\n    getter() {\n      return window.localStorage.getItem('secret') || undefined;\n    }\n  })\n  static secret;\n}\n```\n\n```ts\n// TS\n\nclass Session extends BackendMessage.Session {\n  @attribute('string?', {\n    getter() {\n      return window.localStorage.getItem('secret') || undefined;\n    }\n  })\n  static secret?: string;\n}\n```\n\nSo now, the `Session.secret` attribute can get its value, thanks to the [`getter`](https://layrjs.com/docs/v1/reference/attribute#constructor) option, from the browser's [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).\n\nLastly, we need to make sure that the `Message` component is aware of the new `Session` component. So add the following line inside the `Message` class (preferably at the beginning):\n\n```js\n@provide() static Session = Session;\n```\n\nDone! The administrator should now be able to edit a message.\n\n#### Testing the Application Again\n\nOpen the browser's developer tools, and add the following entry in the local storage:\n\n| Key    | Value  |\n| ------ | ------ |\n| secret | xyz123 |\n\nIf you are using [Chrome](https://www.google.com/chrome/), you should have something like this:\n\n<p>\n\t<img src=\"https://liaison-blog.s3.dualstack.us-west-2.amazonaws.com/images/guestbook-screen-5.png\" alt=\"Screenshot of the Chrome's developer tools showing how to set the secret's value in the local storage\" style=\"width: 100%; margin-top: .5rem\">\n</p>\n\nNow, try to edit a message again, and this time it should work.\n\n#### Polishing the Frontend\n\nEverything works fine, but to improve the user experience, we should hide the \"Edit\" link when the user is not an administrator.\n\nIn the `MessageList()` view of the `Guestbook` component, modify the following lines:\n\n```js\n<div style={{marginTop: '5px'}}>\n  <this.MessageEditor.Link params={message}>Edit</this.MessageEditor.Link>\n</div>\n```\n\nAs follows:\n\n<!-- prettier-ignore -->\n```js\n{Message.Session.secret && (\n  <div style={{marginTop: '5px'}}>\n    <this.MessageEditor.Link params={message}>Edit</this.MessageEditor.Link>\n  </div>\n)}\n```\n\nSo now, the \"Edit\" link should be displayed only when the `Session.secret` is set. You can check that it works by removing the secret from the local storage and reloading the guestbook's home page.\n\n#### Wrapping Up\n\nYou've implemented an authorization mechanism so the guestbook's messages can only be edited by an administrator.\n\nWell done! Our \"Guestbook\" application is now completed. It was pretty easy, wasn't it?\n\nAt this point, you should be able to grasp the main concepts of Layr. More advanced guides will be available soon, but in the meantime, please head over to the \"Reference\" section of the documentation for a comprehensive description of the Layr API.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/introduction/data-storage-1YDg3LGZPF09H3opJUEDSp-edited.immutable.md",
    "content": "### Storing Data\n\nIn the [previous guide](https://layrjs.com/docs/v1/introduction/hello-world), we saw how to implement a minimal application with a frontend and a backend. Now let's add another layer to the stack — a database.\n\nThis time we're going to build a simple \"Guestbook\" application, which allows to store some messages in a database and retrieve them for display on the screen.\n\nAs with the [\"Hello, World!\"](https://layrjs.com/docs/v1/introduction/hello-world) application, we will implement a CLI frontend to make things easier. You'll see in the [next guide](https://layrjs.com/docs/v1/introduction/web-app) how to implement a web frontend.\n\n> TLDR: The completed project is available in the <!-- <if language=\"js\"> -->[Layr repository](https://github.com/layrjs/layr/tree/master/examples/v1/guestbook-cli-js)<!-- </if> --><!-- <if language=\"ts\"> -->[Layr repository](https://github.com/layrjs/layr/tree/master/examples/v1/guestbook-cli-ts)<!-- </if> -->.\n\n#### Creating the Project\n\nFirst, from your terminal, create a directory for your project, and navigate into it:\n\n```sh\nmkdir guest-book-cli\ncd guest-book-cli\n```\n\nThen, initialize your project with:\n\n```sh\nnpm init -y\n```\n\n> Note that in a real application, we'd create different packages for the frontend part and the backend part of a full-stack project. But to keep this guide as simple as possible, we're going to put everything in a single package. See the <!-- <if language=\"js\"> -->[\"CRUD Example App\"](https://github.com/layrjs/crud-example-app-js-webpack)<!-- </if> --><!-- <if language=\"ts\"> -->[\"CRUD Example App\"](https://github.com/layrjs/crud-example-app-ts-webpack)<!-- </if> --> repository for an example of application that is organized into multiple packages.\n\n#### Setting Up Your Development Environment\n\nRepeat the same operations as in the [\"Hello, World!\"](https://layrjs.com/docs/v1/introduction/hello-world#setting-up-your-development-environment) application.\n\n#### Implementing the Backend\n\nFirst, install the Layr's packages we're going to use:\n\n```sh\nnpm install @layr/component@1 @layr/component-http-server@1 \\\n  @layr/storable@1 @layr/memory-store@1\n```\n\nIn addition to `@layr/component` and `@layr/component-http-server` that we [already encountered](https://layrjs.com/docs/v1/introduction/hello-world#implementing-the-backend), we've installed:\n\n- `@layr/storable` that provides the [`Storable()`](https://layrjs.com/docs/v1/reference/storable) mixin, which allows to extend a [`Component`](https://layrjs.com/docs/v1/reference/component) class with some storage capabilities.\n- `@layr/memory-store` that provides the [`MemoryStore`](https://layrjs.com/docs/v1/reference/memory-store) class, which is a minimal database storing everything in memory. Obviously, for a real application, you'd like to store your data more permanently, and for that, you can use the [`MongoDBStore`](https://layrjs.com/docs/v1/reference/mongodb-store) class which stores your data in a [MongoDB](https://www.mongodb.com/) database.\n\nNow, create a file named <!-- <if language=\"js\"> -->`backend.js`<!-- </if> --><!-- <if language=\"ts\"> -->`backend.ts`<!-- </if> --> in an `src` directory, and write the following code:\n\n```js\n// JS\n\nimport {Component, expose, validators} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\nimport {MemoryStore} from '@layr/memory-store';\nimport {ComponentHTTPServer} from '@layr/component-http-server';\n\nconst {notEmpty, maxLength} = validators;\n\n@expose({\n  find: {call: true},\n  prototype: {\n    load: {call: true},\n    save: {call: true}\n  }\n})\nexport class Message extends Storable(Component) {\n  @expose({get: true, set: true}) @primaryIdentifier() id;\n\n  @expose({get: true, set: true})\n  @attribute('string', {validators: [notEmpty(), maxLength(300)]})\n  text = '';\n\n  @expose({get: true}) @attribute('Date') createdAt = new Date();\n}\n\nconst store = new MemoryStore();\n\nstore.registerStorable(Message);\n\nconst server = new ComponentHTTPServer(Message, {port: 3210});\n\nserver.start();\n```\n\n```ts\n// TS\n\nimport {Component, expose, validators} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\nimport {MemoryStore} from '@layr/memory-store';\nimport {ComponentHTTPServer} from '@layr/component-http-server';\n\nconst {notEmpty, maxLength} = validators;\n\n@expose({\n  find: {call: true},\n  prototype: {\n    load: {call: true},\n    save: {call: true}\n  }\n})\nexport class Message extends Storable(Component) {\n  @expose({get: true, set: true}) @primaryIdentifier() id!: string;\n\n  @expose({get: true, set: true})\n  @attribute('string', {validators: [notEmpty(), maxLength(300)]})\n  text = '';\n\n  @expose({get: true}) @attribute('Date') createdAt = new Date();\n}\n\nconst store = new MemoryStore();\n\nstore.registerStorable(Message);\n\nconst server = new ComponentHTTPServer(Message, {port: 3210});\n\nserver.start();\n```\n\nSo what's going on there?\n\nThe `Message` class extends the [`Component`](https://layrjs.com/docs/v1/reference/component) class, which is itself extended by the [`Storable()`](https://layrjs.com/docs/v1/reference/storable) mixin, so the messages posted in our guestbook can be stored in a database.\n\nThen, a couple of attributes are defined:\n\n- The `id` attribute uniquely identifies a message. The [`@primaryIdentifier()`](https://layrjs.com/docs/v1/reference/component#primary-identifier-decorator) decorator is used to create a [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/primary-identifier-attribute), which will automatically generate a unique string identifier for each message.\n- The `text` attribute holds the text of a message. The [`@attribute()`](https://layrjs.com/docs/v1/reference/component#attribute-decorator) decorator is used to declare the [type of values](https://layrjs.com/docs/v1/reference/value-type) (`'string'`) the attribute can hold, and some [validators](https://layrjs.com/docs/v1/reference/validator) are specified so a message cannot be empty (`notEmpty()`) or too long (`maxLength(300)`).\n- The `createdAt` attribute holds the creation date of a message. A default value (`new Date()`) is specified so any new message will get the current date automatically.\n\nAll the attributes are exposed to the frontend as follows:\n\n- The `id` attribute can be read and set from the frontend. We need this attribute to be remotely-settable so the identifier of a message can be generated from the frontend. Don't worry, since a primary identifier, once set, cannot be changed, there is no risk that a frontend can maliciously change the `id` of an existing message.\n- The `text` attribute can be read and set from the frontend as well.\n- The `createdAt` attribute can be read from the frontend, but it cannot be set. Since we cannot trust the clock of a frontend, we leave this attribute to get its default value (`new Date()`) from the backend.\n\nJust before the `Message` component definition, the [`@expose()`](https://layrjs.com/docs/v1/reference/component#expose-decorator) decorator is used to expose some methods that are provided by the [`Storable()`](https://layrjs.com/docs/v1/reference/storable) mixin:\n\n- [`Message.find()`](https://layrjs.com/docs/v1/reference/storable#find-class-method) is exposed so the frontend can get a list of the guestbook's messages.\n- [`Message.prototype.load()`](https://layrjs.com/docs/v1/reference/storable#load-instance-method) is exposed so the frontend can load a particular message.\n- [`Message.prototype.save()`](https://layrjs.com/docs/v1/reference/storable#save-instance-method) is exposed so the frontend can add a new message. Note that this method also allows the frontend to update any existing message, and that's probably not what we want. But let's leave that for later when we [handle authorization](https://layrjs.com/docs/v1/introduction/authorization).\n\nOnce the `Message` component is defined, a [`MemoryStore`](https://layrjs.com/docs/v1/reference/memory-store) is created, and the component is registered into it with the [`registerStorable()`](https://layrjs.com/docs/v1/reference/store#register-storable-instance-method) method.\n\nFinally, a [`ComponentHTTPServer`](https://layrjs.com/docs/v1/reference/component-http-server) is created and then started.\n\nStart the backend with the following command:\n\n```sh\n// JS\n\nnpx babel-node ./src/backend.js\n```\n\n```sh\n// TS\n\nnpx ts-node ./src/backend.ts\n```\n\nIf nothing happens on the screen, it's all good. The backend is running and waiting for requests.\n\n#### Implementing the Frontend\n\nFirst, install `@layr/component-http-client`, which will allow us to communicate with the backend.\n\n```sh\nnpm install @layr/component-http-client@1\n```\n\n<!-- <if language=\"ts\"> -->\n\nAlso, since we are going to use some Node.js features, you need to install the corresponding type definitions:\n\n```sh\nnpm install --save-dev @types/node\n```\n\n<!-- </if> -->\n\nThen, create a file named <!-- <if language=\"js\"> -->`frontend.js`<!-- </if> --><!-- <if language=\"ts\"> -->`frontend.ts`<!-- </if> --> in the `src` directory, and write the following code:\n\n```js\n// JS\n\nimport {ComponentHTTPClient} from '@layr/component-http-client';\nimport {Storable} from '@layr/storable';\n\n(async () => {\n  const client = new ComponentHTTPClient('http://localhost:3210', {\n    mixins: [Storable]\n  });\n\n  const Message = await client.getComponent();\n\n  const text = process.argv[2];\n\n  if (text) {\n    addMessage(text);\n  } else {\n    showMessages();\n  }\n\n  async function addMessage(text) {\n    const message = new Message({text});\n    await message.save();\n    console.log(`Message successfully added`);\n  }\n\n  async function showMessages() {\n    const messages = await Message.find(\n      {},\n      {text: true, createdAt: true},\n      {sort: {createdAt: 'desc'}, limit: 30}\n    );\n\n    for (const message of messages) {\n      console.log(`[${message.createdAt.toISOString()}] ${message.text}`);\n    }\n  }\n})();\n```\n\n```ts\n// TS\n\nimport {ComponentHTTPClient} from '@layr/component-http-client';\nimport {Storable} from '@layr/storable';\n\nimport type {Message as MessageType} from './backend';\n\n(async () => {\n  const client = new ComponentHTTPClient('http://localhost:3210', {\n    mixins: [Storable]\n  });\n\n  const Message = (await client.getComponent()) as typeof MessageType;\n\n  const text = process.argv[2];\n\n  if (text) {\n    addMessage(text);\n  } else {\n    showMessages();\n  }\n\n  async function addMessage(text: string) {\n    const message = new Message({text});\n    await message.save();\n    console.log(`Message successfully added`);\n  }\n\n  async function showMessages() {\n    const messages = await Message.find(\n      {},\n      {text: true, createdAt: true},\n      {sort: {createdAt: 'desc'}, limit: 30}\n    );\n\n    for (const message of messages) {\n      console.log(`[${message.createdAt.toISOString()}] ${message.text}`);\n    }\n  }\n})();\n```\n\nIf you've followed the [\"Hello, World!\"](https://layrjs.com/docs/v1/introduction/hello-world) guide, you should be familiar with the beginning of the code which creates a [`ComponentHTTPClient`](https://layrjs.com/docs/v1/reference/component-http-client) and then calls the [`getComponent()`](https://layrjs.com/docs/v1/reference/component-http-client#get-component-instance-method) method to construct the `Message` component. The only difference is that, since the `Message` component is using the [`Storable()`](https://layrjs.com/docs/v1/reference/storable) mixin, we have to pass it to the `ComponentHTTPClient` constructor.\n\nThe rest of the code should be pretty self-explanatory, but let's dive into it anyway so we can better understand some particularities of Layr.\n\nFirst, we get the `text` that can be specified as an argument ([`process.argv[2]`](https://nodejs.org/docs/latest/api/process.html#process_process_argv)) when the user executes the frontend from the terminal.\n\nIf some `text` was specified, we call the `addMessage()` function. Otherwise, we call the `showMessages()` function.\n\nThe `addMessage()` function creates a `Message` instance with the specified text and then calls the [`save()`](https://layrjs.com/docs/v1/reference/storable#save-instance-method) method, which is exposed by the backend and can, therefore, be remotely executed to save the message into the database.\n\nJust before a component is saved, all its validators are automatically invoked, and if one of them fails, an error is thrown. You can, therefore, be assured that all data recorded in the database is valid according to the validators you have set up. So, in our case, all message texts should not be empty and not longer than 300 characters.\n\nThe `showMessages()` function calls the [`find()`](https://layrjs.com/docs/v1/reference/storable#find-class-method) method with three parameters:\n\n- The first parameter (`{}`) specifies the query to be used when searching the messages in the database. In the present case, an empty object is specified, so any message can be returned.\n- The second parameter (`{text: true, createdAt: true}`) specifies which attributes should be returned. Instead of an object describing the attributes to return, we could have specified `true` to return all the attributes of the `Message` component, but it is good practice to specify some attributes explicitly so we avoid data over-fetching when new attributes are added to a component.\n- The third parameter (`{sort: {createdAt: 'desc'}, limit: 30}`) specifies some options to sort the messages by reverse-chronological order and limit the number of messages to 30.\n\nJust like the `save()` method, the `find()` method is exposed by the backend, so it can be remotely executed to retrieve the messages from the database. Note that the `load()` method has to be exposed as well because it is called by the `find()` method under the hood so the `text` and `createdAt` attributes can be loaded. Otherwise, only the primary identifier attribute (`id`) would be accessible.\n\nLet's start the frontend to make sure everything is working properly. While keeping the backend running, invoke the following command in another terminal to add a message in our guestbook:\n\n```sh\n// JS\n\nnpx babel-node ./src/frontend.js \"First message\"\n```\n\n```sh\n// TS\n\nnpx ts-node ./src/frontend.ts \"First message\"\n```\n\nThe following should be printed on the screen:\n\n```sh\nMessage successfully added\n```\n\nNow, invoke the following command to display the messages available in the guestbook:\n\n```sh\n// JS\n\nnpx babel-node ./src/frontend.js\n```\n\n```sh\n// TS\n\nnpx ts-node ./src/frontend.ts\n```\n\nThe output should be something like this:\n\n```sh\n[2020-08-17T07:39:07.801Z] First message\n```\n\nCongratulations! Our \"Guestbook\" application is complete, and hopefully, you should start seeing how easy it is to build a full-stack application with Layr.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/introduction/hello-world-7F76XnjXXBB7eeZTpEZwEX-edited.immutable.md",
    "content": "### Hello, World!\n\nLet's start our journey into Layr by implementing the mandatory [\"Hello, World!\"](https://en.wikipedia.org/wiki/%22Hello,_World!%22_program) program, and let's make it object-oriented and full-stack!\n\n> Layr supports both [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript) and [TypeScript](https://www.typescriptlang.org/). To select your language of choice, use the drop-down menu on the left.\n\n> TLDR: The completed project is available in the <!-- <if language=\"js\"> -->[Layr repository](https://github.com/layrjs/layr/tree/master/examples/v1/hello-world-js)<!-- </if> --><!-- <if language=\"ts\"> -->[Layr repository](https://github.com/layrjs/layr/tree/master/examples/v1/hello-world-ts)<!-- </if> -->.\n\n#### Creating the Project\n\nFirst, from your terminal, create a directory for your project, and navigate into it:\n\n```sh\nmkdir hello-world\ncd hello-world\n```\n\nThen, initialize your project with:\n\n```sh\nnpm init -y\n```\n\n> Note that in a real application, we'd create different packages for the frontend part and the backend part of a full-stack project. But to keep this guide as simple as possible, we're going to put everything in a single package. See the <!-- <if language=\"js\"> -->[\"CRUD Example App\"](https://github.com/layrjs/crud-example-app-js-webpack)<!-- </if> --><!-- <if language=\"ts\"> -->[\"CRUD Example App\"](https://github.com/layrjs/crud-example-app-ts-webpack)<!-- </if> --> repository for an example of application that is organized into multiple packages.\n\n#### Setting Up Your Development Environment\n\n<!-- <if language=\"js\"> -->\n\nSince Layr is using some novel JavaScript features (such as decorators), you need to install [Babel](https://babeljs.io/) to compile the code we're going to write:\n\n```sh\nnpm install --save-dev @babel/core @babel/node @babel/preset-env \\\n  @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators\n```\n\n> Note that we've installed [`@babel/node`](https://babeljs.io/docs/en/babel-node) to simplify the development workflow. When deploying to production, instead of using `@babel/node`, you'd better compile your code in a build step so it can be directly executed with [Node.js](https://nodejs.org/).\n\nFinally, configure Babel by creating a `babel.config.json` file with the following content:\n\n```json\n{\n  \"presets\": [[\"@babel/preset-env\", {\"targets\": {\"node\": \"10\"}}]],\n  \"plugins\": [\n    [\"@babel/plugin-proposal-decorators\", {\"legacy\": true}],\n    [\"@babel/plugin-proposal-class-properties\"]\n  ]\n}\n```\n\n<!-- </if> -->\n\n<!-- <if language=\"ts\"> -->\n\nThen, install the necessary packages to compile and execute the [TypeScript](https://www.typescriptlang.org/) code we're going to write:\n\n```sh\nnpm install --save-dev typescript ts-node\n```\n\n> Note that we've installed [`ts-node`](https://www.npmjs.com/package/ts-node) to simplify the development workflow. When deploying to production, instead of using `ts-node`, you'd better compile your code in a build step so it can be directly executed with [Node.js](https://nodejs.org/).\n\nFinally, configure TypeScript by creating a `tsconfig.json` file with the following content:\n\n```json\n{\n  \"include\": [\"src/**/*\"],\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"module\": \"CommonJS\",\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"sourceMap\": true,\n    \"strict\": true,\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"experimentalDecorators\": true,\n    \"skipLibCheck\": true\n  }\n}\n```\n\n<!-- </if> -->\n\n#### Implementing the Backend\n\nOur full-stack \"Hello, World!\" will be composed of two parts:\n\n- A backend in charge of the data model and business logic.\n- A frontend in charge of the user interface.\n\nSure, such an architecture sounds silly for a simple \"Hello, World!\". But it serves our purpose, which is to introduce the core concepts of Layr.\n\nSo let's start by implementing the backend.\n\nFirst, install the Layr's packages we're going to use:\n\n```sh\nnpm install @layr/component@1 @layr/component-http-server@1\n```\n\nWe've installed:\n\n- `@layr/component` that provides the [`Component`](https://layrjs.com/docs/v1/reference/component) class, which can be conceptualized as the basic JavaScript [`Object`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) class, but with superpowers.\n- `@layr/component-http-server` that provides the [`ComponentHTTPServer`](https://layrjs.com/docs/v1/reference/component-http-server) class, which allows to serve a `Component` class over HTTP.\n\nNow, let's write some actual code. With your favorite code editor, create a file named <!-- <if language=\"js\"> -->`backend.js`<!-- </if> --><!-- <if language=\"ts\"> -->`backend.ts`<!-- </if> --> in an `src` directory, and write the following code:\n\n```js\nimport {Component, attribute, method, expose} from '@layr/component';\nimport {ComponentHTTPServer} from '@layr/component-http-server';\n\nexport class Greeter extends Component {\n  @expose({set: true}) @attribute('string') name = 'World';\n\n  @expose({call: true}) @method() async hello() {\n    return `Hello, ${this.name}!`;\n  }\n}\n\nconst server = new ComponentHTTPServer(Greeter, {port: 3210});\n\nserver.start();\n```\n\nOh my! All that code just for a simple \"Hello, World!\"? Sure, it sounds overkill, but we've actually implemented a full-grade backend with a data model, some business logic, and an HTTP server exposing the whole thing.\n\nLet's decompose the code to understand what we've accomplished.\n\nFirst, we've defined a `Greeter` class that inherits from [`Component`](https://layrjs.com/docs/v1/reference/component). In a nutshell, by inheriting a class from `Component`, you get most of the goodness provided by Layr, such as attribute type checking, validation, serialization, or remote method invocation.\n\nOur `Greeter` class is composed of two properties:\n\n- The `name` attribute prefixed with the [`@attribute()`](https://layrjs.com/docs/v1/reference/component#attribute-decorator) decorator. This decorator enables (among other things) to specify the type of values an attribute can store (`'string'` in our case). Notice that the `name` attribute also specifies a default value (`'World'`).\n- The `hello()` method prefixed with the [`@method()`](https://layrjs.com/docs/v1/reference/component#method-decorator) decorator.\n\nBoth properties are exposed to remote access thanks to the [`@expose()`](https://layrjs.com/docs/v1/reference/component#expose-decorator) decorator:\n\n- The `name` attribute can be remotely set (`{set: true}`).\n- The `hello()` method can be remotely called (`{call: true}`).\n\n> Note that you don't need to prefix all your attributes and methods with a decorator. Typically, you only use a decorator when you want to take profit of a Layr feature.\n\nAfter the class definition, a [`ComponentHTTPServer`](https://layrjs.com/docs/v1/reference/component-http-server) is created and then started with [`server.start()`](https://layrjs.com/docs/v1/reference/component-http-server#start-instance-method).\n\nThis is it! Our backend is completed and ready to be executed with:\n\n```sh\n// JS\n\nnpx babel-node ./src/backend.js\n```\n\n```sh\n// TS\n\nnpx ts-node ./src/backend.ts\n```\n\nIf nothing happens on the screen, it's all good. The backend is running and waiting for requests.\n\n> If you want to display a log of what's going on in the backend, you can set some environment variables before starting it:\n>\n> ```sh\n> // JS\n>\n> DEBUG=layr:* DEBUG_DEPTH=5 npx babel-node ./src/backend.js\n> ```\n>\n> ```sh\n> // TS\n>\n> DEBUG=layr:* DEBUG_DEPTH=5 npx ts-node ./src/backend.ts\n> ```\n\n#### Implementing the frontend\n\nA typical frontend runs in a browser, but to make this guide easier to grasp, we're going to implement a CLI frontend (i.e., a frontend that runs in the terminal).\n\nFirst, install `@layr/component-http-client`, which will allow us to communicate with the backend.\n\n```sh\nnpm install @layr/component-http-client@1\n```\n\nThen, create a file named <!-- <if language=\"js\"> -->`frontend.js`<!-- </if> --><!-- <if language=\"ts\"> -->`frontend.ts`<!-- </if> --> in the `src` directory, and write the following code:\n\n```js\n// JS\n\nimport {ComponentHTTPClient} from '@layr/component-http-client';\n\n(async () => {\n  const client = new ComponentHTTPClient('http://localhost:3210');\n\n  const Greeter = await client.getComponent();\n\n  const greeter = new Greeter({name: 'Steve'});\n\n  console.log(await greeter.hello());\n})();\n```\n\n```ts\n// TS\n\nimport {ComponentHTTPClient} from '@layr/component-http-client';\n\nimport type {Greeter as GreeterType} from './backend';\n\n(async () => {\n  const client = new ComponentHTTPClient('http://localhost:3210');\n\n  const Greeter = (await client.getComponent()) as typeof GreeterType;\n\n  const greeter = new Greeter({name: 'Steve'});\n\n  console.log(await greeter.hello());\n})();\n```\n\nThat wasn't too difficult, was it? Well, actually, with these few lines of code, there's quite a lot going on.\n\nFirst, a [`ComponentHTTPClient`](https://layrjs.com/docs/v1/reference/component-http-client) is created so we can communicate with the [`ComponentHTTPServer`](https://layrjs.com/docs/v1/reference/component-http-server) that was created in the backend.\n\nThen, the [`getComponent()`](https://layrjs.com/docs/v1/reference/component-http-client#get-component-instance-method) method is called to get the `Greeter` class from the backend. Well, sort of. In reality, what we're getting is a proxy to the `Greeter` class that is running in the backend. All the exposed attributes of the backend's `Greeter` class become available from the frontend (with their [types](https://layrjs.com/docs/v1/reference/value-type), [validators](https://layrjs.com/docs/v1/reference/validator), default values, etc.), and all the backend's exposed methods are callable from the frontend.\n\n<!-- <if language=\"ts\"> -->\n\nSince we're using TypeScript, we want to make the frontend's `Greeter` class fully aware of its type. We could have repeated the class definition in the frontend, but a better way is to import the backend's `Greeter` class type, and cast the frontend's `Greeter` class to this same type (using the `as typeof GreeterType` expression). Note that only the type is imported from the backend (thanks to the `import type` statement), so the implementation remains totally unknown for the frontend.\n\n<!-- </if> -->\n\nOnce we have the `Greeter` class in the frontend, we can use it like any JavaScript class. So an instance of it is created with `new Greeter({name: 'Steve'})`. Note that a value for the `name` attribute is specified, but we could not specify anything, in which case the default value (`'World'`) would be used.\n\nFinally, the `hello()` method is called, and this is where all the magic happens. Although the method is implemented and executed in the backend, we can call it from the frontend as if it were a regular JavaScript method. In our case, the `hello()` method has no parameters, but if it did, they would be automatically transported to the backend. Even better, the instance's attributes that are set in the frontend (`greeter.name` in our case) are transported to the backend as well.\n\nIt's about time to run the frontend. While keeping the backend running, invoke the following command in a new terminal:\n\n```sh\n// JS\n\nnpx babel-node ./src/frontend.js\n```\n\n```sh\n// TS\n\nnpx ts-node ./src/frontend.ts\n```\n\nAs expected, the following is printed on the screen:\n\n```sh\nHello, Steve!\n```\n\nLet's add a bit of fun by extending the `Greeter` class in the frontend.\n\nModify the <!-- <if language=\"js\"> -->`frontend.js`<!-- </if> --><!-- <if language=\"ts\"> -->`frontend.ts`<!-- </if> --> file as follows:\n\n```js\n// JS\n\nimport {ComponentHTTPClient} from '@layr/component-http-client';\n\n(async () => {\n  const client = new ComponentHTTPClient('http://localhost:3210');\n\n  const BackendGreeter = await client.getComponent();\n\n  class Greeter extends BackendGreeter {\n    async hello() {\n      return (await super.hello()).toUpperCase();\n    }\n  }\n\n  const greeter = new Greeter({name: 'Steve'});\n\n  console.log(await greeter.hello());\n})();\n```\n\n```ts\n// TS\n\nimport {ComponentHTTPClient} from '@layr/component-http-client';\n\nimport type {Greeter as GreeterType} from './backend';\n\n(async () => {\n  const client = new ComponentHTTPClient('http://localhost:3210');\n\n  const BackendGreeter = (await client.getComponent()) as typeof GreeterType;\n\n  class Greeter extends BackendGreeter {\n    async hello() {\n      return (await super.hello()).toUpperCase();\n    }\n  }\n\n  const greeter = new Greeter({name: 'Steve'});\n\n  console.log(await greeter.hello());\n})();\n```\n\nAs mentioned before a class proxy (`BackendGreeter`) behaves like a regular class, and it is totally fine to extend it. So the frontend's `hello()` method can be overridden to transform the result of the backend's `hello()` method, which is simply called with `super.hello()`.\n\nIf you run the frontend again, you should now get the following output:\n\n```sh\nHELLO, STEVE!\n```\n\nLayr brings what we like to call a \"cross-layer component inheritance\" ability. Instead of seeing the frontend and the backend as two separate worlds, you can see them as one unified world. Naturally, the frontend and the backend remain _physically_ separated. They run in two different execution environments. But _logically_, they are one thing, and that changes the game completely.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/introduction/introduction-1NNtkbXN0zHvG6VR0jvPRK-edited.immutable.md",
    "content": "### Introduction\n\nLayr is a set of JavaScript/TypeScript libraries to dramatically simplify the development of full-stack applications.\n\nTypically, a full-stack application is composed of a frontend and a backend running in two different environments that are connected through a web API (REST, GraphQL, etc.)\n\nSeparating the frontend and the backend is a good thing, but the problem is that building a web API usually leads to a lot of code scattering, duplication of knowledge, boilerplate, and accidental complexity.\n\nLayr removes the need of building a web API and [reunites](https://layrjs.com/blog/articles/Simplify-Full-Stack-Development-with-a-Unified-Architecture-187fr1) the frontend and the backend in a way that you can experience them as a single entity.\n\nOn the frontend side, Layr gives you [routing capabilities](https://layrjs.com/docs/v1/reference/routable) and [object observability](https://layrjs.com/docs/v1/reference/observable) so that in most cases you don't need to add an external router or a state manager.\n\nLast but not least, Layr offers an [ORM](https://layrjs.com/docs/v1/reference/storable) to make data storage as easy as possible.\n\n#### Core Features\n\nLayr provides everything you need to build a full-stack application from start to finish:\n\n- **Cross-layer inheritance:** a frontend class can \"inherit\" from a backend class so that some [attributes](https://layrjs.com/docs/v1/reference/attribute) can be automatically transported between the frontend and the backend, and some backend's [methods](https://layrjs.com/docs/v1/reference/method) can be easily called from the frontend.\n- **Controlled attributes:** an attribute can be [type-checked](https://layrjs.com/docs/v1/reference/value-type) at runtime, [validated](https://layrjs.com/docs/v1/reference/validator), [serialized](https://layrjs.com/docs/v1/reference/component#serialization), and [observed](https://layrjs.com/docs/v1/reference/observable).\n- **Remote method invocation:** a backend's method can be [exposed](https://layrjs.com/docs/v1/reference/component#expose-decorator) so that the frontend can call it without the need to build a web API.\n- **Storage:** a class instance can be [persisted](https://layrjs.com/docs/v1/reference/storable) in a database. Currently, only [MongoDB](https://www.mongodb.com/) is supported, but more databases will be added soon.\n- **Routing:** a method can be [associated with an URL](https://layrjs.com/docs/v1/reference/routable) and controlled by a [router](https://layrjs.com/docs/v1/reference/router) so that this method is automatically called when the user navigates.\n- **Authorization:** [role-based](https://layrjs.com/docs/v1/reference/with-roles) authorizations can be set to restrict an attribute or a method for some users.\n- **Interoperability:** the backend is exposed through a [Deepr API](https://deepr.io) so that you can consume it from any frontend even though it's not built with Layr. And if you want to bring a more traditional API (e.g., REST) to your backend, it's very easy to build such an API on top of your Layr backend.\n- **Integrations:** integration helpers are provided to facilitate the integration of the most popular libraries or services. Currently, only two integration helpers are available: [react-integration](https://layrjs.com/docs/v1/reference/react-integration) and [aws-integration](https://layrjs.com/docs/v1/reference/aws-integration). But more should come shortly.\n\n#### Core Principles\n\nHere's a quick taste of the core principles upon which Layr is built:\n\n- **Object-oriented:** Layr embraces the object-oriented approach in all aspects of an application and allows you to organize your code in a way that is as cohesive as possible.\n- **Low-level:** Layr is designed to be as closest as possible to the language and in many ways, it can be seen as a [language extension](https://layrjs.com/blog/articles/Getting-the-Right-Level-of-Generalization-7xpk37).\n- **Unopinionated:** Layr has strong opinions about itself but doesn't force you to use any external libraries, services, or tools.\n\n#### Compatibility\n\nLayr is implemented in [TypeScript](https://www.typescriptlang.org/) but you can use either JavaScript or TypeScript to build your application.\n\nIf you are using JavaScript, you'll need to compile your code with [Babel](https://babeljs.io/) to take advantage of some novel JavaScript features such as \"decorators\".\n\nIf you are using TypeScript, all you need is the TypeScript compiler.\n\nTo run your application, you'll need a JavaScript runtime for both the frontend and the backend.\n\n##### Frontend\n\n###### Web\n\nAny modern browser should work fine.\n\nHere are the minimum versions with which Layr is tested:\n\n- Chrome v55\n- Safari v11\n- Firefox v54\n- Edge Chromium\n\n###### Mobile and Desktop\n\nAny mobile or desktop application framework using JavaScript (such as [React Native](https://reactnative.dev/) or [Electron](https://www.electronjs.org/)) should work fine.\n\n##### Backend\n\nAny environment running [Node.js](https://nodejs.org/) v10 or later is supported.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/introduction/routing-4ubaWloMHuNNs0foagYUwi-edited.immutable.md",
    "content": "### Bringing Some Routes\n\nSo far, our [\"Guestbook\"](https://layrjs.com/docs/v1/introduction/web-app) application was displaying all its content on a single page, so we didn't have to use routes to match different URLs to different pages.\n\nBut now, we're going to add a new page that will allow the user to edit an existing message. So, we'll need some routes:\n\n- `'/'` for the \"home\" page.\n- `'/messages/:id'` for the \"edit message\" page.\n\nLet's see how to achieve that with Layr.\n\n> TLDR: The completed project is available in the <!-- <if language=\"js\"> -->[Layr repository](https://github.com/layrjs/layr/tree/master/examples/v1/guestbook-web-with-routes-js)<!-- </if> --><!-- <if language=\"ts\"> -->[Layr repository](https://github.com/layrjs/layr/tree/master/examples/v1/guestbook-web-with-routes-ts)<!-- </if> -->.\n\n#### Preparing the Project\n\nYou can duplicate the [previous project](https://layrjs.com/docs/v1/introduction/web-app) or simply modify it in place.\n\n#### Adding a Route to the Frontend\n\nSince we're building a [single-page application](https://en.wikipedia.org/wiki/Single-page_application), the routes have to be implemented in the frontend, and the backend will remain unchanged.\n\nIn Layr, a route is simply a method with a URL. Once a method has a URL, it can be called by this URL, and the outcome is the same as if it was called directly.\n\n##### Making the `Guestbook` component \"routable\"\n\nTo associate a URL with a method of the `Guestbook` [component](https://layrjs.com/docs/v1/reference/component), we need this component to be \"routable\", and to do so we're going to use the [`Routable()`](https://layrjs.com/docs/v1/reference/routable) mixin.\n\nFirst, install the `@layr/routable` package:\n\n```sh\nnpm install @layr/routable@1\n```\n\nNext, add the following line at the beginning of the <!-- <if language=\"js\"> -->`src/frontend.js`<!-- </if> --><!-- <if language=\"ts\"> -->`src/frontend.tsx`<!-- </if> --> file to import the [`Routable()`](https://layrjs.com/docs/v1/reference/routable) mixin and the [`@route()`](https://layrjs.com/docs/v1/reference/routable#route-decorator) decorator:\n\n```js\nimport {Routable, route} from '@layr/routable';\n```\n\nNext, change the `Guestbook` class definition as follows:\n\n```js\nclass Guestbook extends Routable(Component) {\n  // ...\n}\n```\n\nNow that the `Guestbook` component is routable, we can use the [`@route()`](https://layrjs.com/docs/v1/reference/routable#route-decorator) decorator to associate a URL to any of its class methods.\n\nChange the `Home()` method as follows:\n\n```js\n@route('/') @view() static Home() {\n  return (\n    <div>\n      <this.MessageList />\n      <this.MessageCreator />\n    </div>\n  );\n}\n```\n\nVoilà! Our `Home()` method (which is also a view) is now associated to the `'/'` URL, and it can be called with the Routable's [`callRouteByURL()`](https://layrjs.com/docs/v1/reference/routable#call-route-by-url-class-method) method:\n\n```js\nGuestbook.callRouteByURL('/'); // => Some React elements\n```\n\nWhich is the same as:\n\n```js\nGuestbook.Home(); // => Some React elements\n```\n\nHowever, in a typical web application, you'll rarely call a method by using the `callRouteByURL()` method. Instead, you'll use a [router](https://layrjs.com/docs/v1/reference/router) to automatically call a route when the user navigates.\n\n##### Adding a Router\n\nFirst, let's import the [`useBrowserRouter()`](https://layrjs.com/docs/v1/reference/react-integration#use-browser-router-react-hook) React hook from the `@layr/react-integration` package:\n\n```js\nimport {useBrowserRouter} from '@layr/react-integration';\n```\n\nThen, let's implement a new `Guestbook`'s view that will be in charge of creating a [`BrowserRouter`](https://layrjs.com/docs/v1/reference/browser-router) (with [`useBrowserRouter()`](https://layrjs.com/docs/v1/reference/react-integration#use-browser-router-react-hook)), calling the current route (with [`callCurrentRoute()`](https://layrjs.com/docs/v1/reference/router#call-current-route-instance-method)), and rendering the result (`content`) inside a minimal layout:\n\n```js\n@view() static Root() {\n  const [router, isReady] = useBrowserRouter(this);\n\n  if (!isReady) {\n    return null;\n  }\n\n  const content = router.callCurrentRoute({\n    fallback: () => 'Sorry, there is nothing here.'\n  });\n\n  return (\n    <div style={{maxWidth: '700px', margin: '40px auto'}}>\n      <h1>Guestbook</h1>\n      {content}\n    </div>\n  );\n}\n```\n\nThis view is named \"Root\" because it will be the new root view of our application. So all we have to do now is to change the view that is mounted by React when the application starts.\n\nTo do so, change the following line:\n\n```js\nReactDOM.render(<Guestbook.Home />, document.getElementById('root'));\n```\n\nAs follows:\n\n```js\nReactDOM.render(<Guestbook.Root />, document.getElementById('root'));\n```\n\nCongrats! Our application is now equipped with a router, and it's a great step forward because we can now add new pages. But before that, let's start the application to make sure we didn't break anything.\n\n#### Starting the Application\n\nFirst, start the backend:\n\n```sh\n// JS\n\nnpx babel-node ./src/backend.js\n```\n\n```sh\n// TS\n\nnpx ts-node ./src/backend.ts\n```\n\nNext, start the frontend in another terminal:\n\n```sh\nnpx webpack serve --mode=development\n```\n\nLastly, open [http://localhost:8080/](http://localhost:8080/) in a browser, and if everything went well, you should see the same display as [before](https://layrjs.com/docs/v1/introduction/web-app#starting-the-frontend).\n\n#### Adding the \"Edit Message\" Page\n\nNow that we have a router in place, it is easy to add the \"edit message\" page. All we have to do is to implement a new view and use the [`@route()`](https://layrjs.com/docs/v1/reference/routable#route-decorator) decorator to provide a URL.\n\nFirst, import a new React hook that we're going to use:\n\n```js\nimport {useAsyncMemo} from '@layr/react-integration';\n```\n\nThen, add the following view inside the `Guestbook` component:\n\n```js\n// JS\n\n@route('/messages/:id') @view() static MessageEditor({id}) {\n  const {Message} = this;\n\n  const [\n    {existingMessage, editedMessage} = {},\n    isLoading,\n    loadingError\n  ] = useAsyncMemo(async () => {\n    const existingMessage = await Message.get(id, {text: true});\n    const editedMessage = existingMessage.fork();\n    return {existingMessage, editedMessage};\n  }, [id]);\n\n  const saveMessage = useCallback(async () => {\n    await editedMessage.save();\n    existingMessage.merge(editedMessage);\n    this.Home.navigate();\n  }, [existingMessage, editedMessage]);\n\n  if (isLoading) {\n    return null;\n  }\n\n  if (loadingError) {\n    return (\n      <p style={{color: 'red'}}>\n        Sorry, an error occurred while loading a guestbook’s message.\n      </p>\n    );\n  }\n\n  return (\n    <div>\n      <h2>Edit a Message</h2>\n      <editedMessage.Form onSubmit={saveMessage} />\n    </div>\n  );\n}\n```\n\n```ts\n// TS\n\n@route('/messages/:id') @view() static MessageEditor({id}: {id: string}) {\n  const {Message} = this;\n\n  const [\n    {existingMessage, editedMessage} = {} as const,\n    isLoading\n  ] = useAsyncMemo(async () => {\n    const existingMessage = await Message.get(id, {text: true});\n    const editedMessage = existingMessage.fork();\n    return {existingMessage, editedMessage};\n  }, [id]);\n\n  const saveMessage = useCallback(async () => {\n    await editedMessage!.save();\n    existingMessage!.merge(editedMessage!);\n    this.Home.navigate();\n  }, [existingMessage, editedMessage]);\n\n  if (isLoading) {\n    return null;\n  }\n\n  if (editedMessage === undefined) {\n    return (\n      <p style={{color: 'red'}}>\n        Sorry, an error occurred while loading a guestbook’s message.\n      </p>\n    );\n  }\n\n  return (\n    <div>\n      <h2>Edit a Message</h2>\n      <editedMessage.Form onSubmit={saveMessage} />\n    </div>\n  );\n}\n```\n\nBesides some React code, we've used some new Layr's features that deserve an explanation.\n\nThe `MessageEditor()` view is associated to the `'/messages/:id'` [URL pattern](https://layrjs.com/docs/v1/reference/route#url-pattern-type), where the `:id` part represents a parameter specifying the `id` of a message. So, for example, if the user navigates to `'/messages/abc123'`, the `MessageEditor()` method will be called with the following parameter: `{id: 'abc123'}`.\n\nWe use the [`useAsyncMemo()`](https://layrjs.com/docs/v1/reference/react-integration#use-async-memo-react-hook) hook to track the loading of the message. Right after the message was loaded (`existingMessage`) with the `StorableComponent`'s [`get()`](https://layrjs.com/docs/v1/reference/storable#get-class-method) method, we fork it (`editedMessage`) by using the Component's [`fork()`](https://layrjs.com/docs/v1/reference/component#fork-instance-method) method. Component forking is a unique feature of Layr, and, in a nutshell, it is like copying, except that it is extremely fast and memory efficient.\n\nTo explain why we need to fork the loaded message, we need to talk a bit about how Layr is managing the instances of a component. When a component instance has a [primary identifier](https://layrjs.com/docs/v1/reference/primary-identifier-attribute) (e.g., an `id` attribute), it is managed by an [identity map](https://layrjs.com/docs/v1/reference/identity-map) that ensures that in the whole application there can only be one component instance with a specific identifier.\n\nSo, for example, if we load the same message twice, we get two references to the same `Message` instance:\n\n```js\nconst message1 = await Message.get({id: 'abc123'});\nconst message2 = await Message.get({id: 'abc123'});\n\nmessage1 === message2; // => true\n```\n\nTherefore, when we load a message in the `MessageEditor()` view, we are getting the same `Message` instance that was previously loaded in the `MessageList()` view. However, we don't want the user to modify the message that is displayed in the `MessageList()` view until it is saved to the backend, and this is why we have to fork it.\n\nThe `saveMessage()` callback does three things:\n\n1. It [saves](https://layrjs.com/docs/v1/reference/storable#save-instance-method) the message fork (`editedMessage`) to the backend.\n2. It [merges](https://layrjs.com/docs/v1/reference/component#merge-instance-method) the message fork (`editedMessage`) into the original message (`existingMessage`) so the changes made by the user can be reflected in the `MessageList()` view.\n3. It [navigates](https://layrjs.com/docs/v1/reference/router#navigate-instance-method) to the Guestbook's `Home` view.\n\nThe rest of the `MessageEditor()` view is just regular React code.\n\n#### Navigating to the `MessageEditor()` View\n\nThere is only one thing left to do. We need to display a link so the user can navigate to the `MessageEditor()` view.\n\nIn the `MessageList()` view, add the following lines right after the `<message.Viewer />` JSX expression:\n\n```js\n<div style={{marginTop: '5px'}}>\n  <this.MessageEditor.Link params={message}>Edit</this.MessageEditor.Link>\n</div>\n```\n\nWhat's going on there? `MessageEditor()` being both a view (thanks to the the [`@view()`](https://layrjs.com/docs/v1/reference/react-integration#view-decorator) decorator) and a route (thanks to the [`@route()`](https://layrjs.com/docs/v1/reference/routable#route-decorator) decorator), we're automatically getting an handy [`Link()`](https://layrjs.com/docs/v1/reference/routable#route-decorator) shortcut function that we can use to display a link (rendered as an `<a>` HTML tag) so the user can navigate to the `MessageEditor()` view.\n\nHow does the `MessageEditor()` view know which message to edit? The current `message` being specified in the `params` attribute of the [`Link()`](https://layrjs.com/docs/v1/reference/routable#route-decorator) shortcut function, the `MessageEditor()` view is executed with the `message` as first parameter.\n\n#### Testing the Application\n\nYou should now see an \"Edit\" link below each message:\n\n<p>\n\t<img src=\"https://liaison-blog.s3.dualstack.us-west-2.amazonaws.com/images/guestbook-screen-3.png\" alt=\"Screenshot of the guestbook app showing an 'Edit' link\" style=\"width: 100%; margin-top: .5rem\">\n</p>\n\nAnd when you click on an \"Edit\" link, you should see something like this:\n\n<p>\n\t<img src=\"https://liaison-blog.s3.dualstack.us-west-2.amazonaws.com/images/guestbook-screen-4.png\" alt=\"Screenshot of the guestbook app showing the 'Edit Message' page\" style=\"width: 100%; margin-top: .5rem\">\n</p>\n\nModify a message, click the \"Submit\" button, and you should be back at the home page showing the modified message.\n\n#### Wrapping Up\n\nYou've added a new page to our \"Guestbook\" application so the user can edit a message, and along the way, you've discovered how to implement routing by using a [`BrowserRouter`](https://layrjs.com/docs/v1/reference/browser-router) and some [routes](https://layrjs.com/docs/v1/reference/route). Also, you got a glimpse of some of the most powerful features of Layr — [component forking](https://layrjs.com/docs/v1/reference/component#forking) and [identity mapping](https://layrjs.com/docs/v1/reference/identity-map).\n"
  },
  {
    "path": "website/frontend/public/docs/v1/introduction/web-app-5okryUyuriFFHXtFK9uZNY-edited.immutable.md",
    "content": "### Building a Web App\n\nIn the [previous guide](https://layrjs.com/docs/v1/introduction/data-storage), we saw how to implement a simple \"Guestbook\" application with a CLI frontend, a backend, and a database. Now we're going to improve the user experience by replacing the CLI frontend with a web frontend.\n\n> TLDR: The completed project is available in the <!-- <if language=\"js\"> -->[Layr repository](https://github.com/layrjs/layr/tree/master/examples/v1/guestbook-web-js)<!-- </if> --><!-- <if language=\"ts\"> -->[Layr repository](https://github.com/layrjs/layr/tree/master/examples/v1/guestbook-web-ts)<!-- </if> -->.\n\n#### Preparing the Project\n\nSince we're going to use the same backend as before, you can duplicate the [previous project](https://layrjs.com/docs/v1/introduction/data-storage) or simply modify it in place.\n\n#### Setting Up Your Development Environment\n\n<!-- <if language=\"js\"> -->\n\n##### Babel\n\nWe're going to use [React](https://reactjs.org/) to build the web frontend, so we need to configure [Babel](https://babeljs.io/) accordingly.\n\n> Note that we've chosen to use React because we think it fits well in the context of an application built with Layr. But Layr is in no way dependent on React, and you are free to use the frontend library of your choice.\n\nFirst, install the `@babel/preset-react` package:\n\n```sh\nnpm install --save-dev @babel/preset-react\n```\n\nThen, modify the `babel.config.json` file as follows:\n\n```json\n{\n  \"presets\": [\n    [\"@babel/preset-env\", {\"targets\": {\"node\": \"10\"}}],\n    \"@babel/preset-react\"\n  ],\n  \"plugins\": [\n    [\"@babel/plugin-proposal-decorators\", {\"legacy\": true}],\n    [\"@babel/plugin-proposal-class-properties\"]\n  ]\n}\n```\n\n<!-- </if> -->\n\n<!-- <if language=\"ts\"> -->\n\n##### TypeScript\n\nWe're going to use [React](https://reactjs.org/) to build the web frontend, so we need to configure the [TypeScript](https://www.typescriptlang.org/) compiler accordingly.\n\n> Note that we've chosen to use React because we think it fists well in the context of an application built with Layr. But Layr is in no way dependent on React, and you are free to use the frontend library of your choice.\n\nModify the `tsconfig.json` file as follows:\n\n```json\n{\n  \"include\": [\"src/**/*\"],\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"module\": \"CommonJS\",\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"jsx\": \"react\",\n    \"sourceMap\": true,\n    \"strict\": true,\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"experimentalDecorators\": true,\n    \"skipLibCheck\": true\n  }\n}\n```\n\n<!-- </if> -->\n\n##### Webpack\n\nWe'll use [Webpack](https://webpack.js.org/) to bundle the code of the frontend, so let's install the required packages:\n\n```sh\n// JS\n\nnpm install --save-dev webpack webpack-cli webpack-dev-server \\\n  babel-loader html-webpack-plugin\n```\n\n```sh\n// TS\n\nnpm install --save-dev webpack webpack-cli webpack-dev-server \\\n  ts-loader html-webpack-plugin\n```\n\nNext, create a `webpack.config.js` file at the root of your project with the following content:\n\n```js\n// JS\n\nconst webpack = require('webpack');\nconst HtmlWebPackPlugin = require('html-webpack-plugin');\nconst path = require('path');\n\nmodule.exports = (env, argv) => {\n  return {\n    // The entry point of the app is './src/frontend.js'\n    entry: './src/frontend.js',\n    output: {\n      // Specify '/' as the base path for all the assets\n      // This is required for a single-page application\n      publicPath: '/'\n    },\n    module: {\n      rules: [\n        {\n          // Use 'babel-loader' to compile the JS files\n          test: /\\.js$/,\n          include: path.join(__dirname, 'src'),\n          loader: 'babel-loader'\n        }\n      ]\n    },\n    plugins: [\n      // Use 'html-webpack-plugin' to generate the 'index.html' file\n      // from the './src/index.html' template\n      new HtmlWebPackPlugin({\n        template: './src/index.html',\n        inject: false\n      })\n    ],\n    // Generate source maps to make debugging easier\n    devtool: 'eval-cheap-module-source-map',\n    devServer: {\n      // Fallback to 'index.html' in case of 404 responses\n      // This is required for a single-page application\n      historyApiFallback: true\n    }\n  };\n};\n```\n\n```js\n// TS\n\nconst webpack = require('webpack');\nconst HtmlWebPackPlugin = require('html-webpack-plugin');\nconst path = require('path');\n\nmodule.exports = (env, argv) => {\n  return {\n    // The entry point of the app is './src/frontend.tsx'\n    entry: './src/frontend.tsx',\n    output: {\n      // Specify '/' as the base path for all the assets\n      // This is required for a single-page application\n      publicPath: '/'\n    },\n    module: {\n      rules: [\n        {\n          // Use 'ts-loader' to compile the TS files\n          test: /\\.tsx?$/,\n          include: path.join(__dirname, 'src'),\n          loader: 'ts-loader'\n        }\n      ]\n    },\n    plugins: [\n      // Use 'html-webpack-plugin' to generate the 'index.html' file\n      // from the './src/index.html' template\n      new HtmlWebPackPlugin({\n        template: './src/index.html',\n        inject: false\n      })\n    ],\n    // Generate source maps to make debugging easier\n    devtool: 'eval-cheap-module-source-map',\n    devServer: {\n      // Fallback to 'index.html' in case of 404 responses\n      // This is required for a single-page application\n      historyApiFallback: true\n    }\n  };\n};\n```\n\n> Note that this is a \"minimal\" Webpack configuration for a simple development environment. You'll need a slightly more advanced configuration to generate a bundle that is suitable for production deployment. See the <!-- <if language=\"js\"> -->[\"CRUD Example App\"](https://github.com/layrjs/crud-example-app-js-webpack)<!-- </if> --><!-- <if language=\"ts\"> -->[\"CRUD Example App\"](https://github.com/layrjs/crud-example-app-ts-webpack)<!-- </if> --> repository for an example of a configuration that works pretty well for both development and production.\n\n#### Starting the Backend\n\nThe [backend](https://layrjs.com/docs/v1/introduction/data-storage#implementing-the-backend) is fine as it is, so all you need to do is to start it if it isn't already running:\n\n```sh\n// JS\n\nnpx babel-node ./src/backend.js\n```\n\n```sh\n// TS\n\nnpx ts-node ./src/backend.ts\n```\n\n#### Reimplementing the Frontend\n\nWe're going to transform the previous [CLI frontend](https://layrjs.com/docs/v1/introduction/data-storage#implementing-the-frontend) into a web frontend, and we'll use [React](https://reactjs.org/) to take care of the UI rendering.\n\nFirst, install the required packages:\n\n```sh\n// JS\n\nnpm install react react-dom @layr/react-integration@1\n```\n\n```sh\n// TS\n\nnpm install react react-dom @layr/react-integration@1\nnpm install --save-dev @types/react @types/react-dom\n```\n\nNote that in addition to React, we've also installed [`@layr/react-integration`](https://layrjs.com/docs/v1/reference/react-integration) to simplify the use of React inside Layr components.\n\nNext, create an `index.html` file in the `src` directory, and write the following content:\n\n<!-- prettier-ignore -->\n```html\n<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Guestbook</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta http-equiv=\"x-ua-compatible\" content=\"ie=edge\" />\n    <%= htmlWebpackPlugin.tags.headTags %>\n  </head>\n  <body>\n    <noscript><p>Sorry, this site requires JavaScript to be enabled.</p></noscript>\n    <div id=\"root\"></div>\n    <%= htmlWebpackPlugin.tags.bodyTags %>\n  </body>\n</html>\n```\n\nThere is nothing particularly remarkable so far. We've just created a basic HTML file that serves as a shell for our application.\n\nNow that all the boring stuff is in place, we can finally get into the \"meat\" of the application — the <!-- <if language=\"js\"> -->JavaScript<!-- </if> --><!-- <if language=\"ts\"> -->TypeScript<!-- </if> --> code.\n\n<!-- <if language=\"js\"> -->\n\nModify the `src/frontend.js` file as follows:\n\n<!-- </if> -->\n\n<!-- <if language=\"ts\"> -->\n\nRename the `src/frontend.ts` file to `src/frontend.tsx` and modify its content as follows:\n\n<!-- </if> -->\n\n```js\n// JS\n\nimport React, {useCallback} from 'react';\nimport ReactDOM from 'react-dom';\nimport {Component, attribute, provide} from '@layr/component';\nimport {Storable} from '@layr/storable';\nimport {ComponentHTTPClient} from '@layr/component-http-client';\nimport {\n  view,\n  useAsyncCall,\n  useAsyncCallback,\n  useRecomputableMemo\n} from '@layr/react-integration';\n\nasync function main() {\n  const client = new ComponentHTTPClient('http://localhost:3210', {\n    mixins: [Storable]\n  });\n\n  const BackendMessage = await client.getComponent();\n\n  class Message extends BackendMessage {\n    @view() Viewer() {\n      return (\n        <div>\n          <small>{this.createdAt.toLocaleString()}</small>\n          <br />\n          <strong>{this.text}</strong>\n        </div>\n      );\n    }\n\n    @view() Form({onSubmit}) {\n      const [handleSubmit, isSubmitting, submitError] = useAsyncCallback(\n        async (event) => {\n          event.preventDefault();\n          await onSubmit();\n        }\n      );\n\n      return (\n        <form onSubmit={handleSubmit}>\n          <div>\n            <textarea\n              value={this.text}\n              onChange={(event) => {\n                this.text = event.target.value;\n              }}\n              required\n              style={{width: '100%', height: '80px'}}\n            />\n          </div>\n\n          <p>\n            <button type=\"submit\" disabled={isSubmitting}>\n              Submit\n            </button>\n          </p>\n\n          {submitError && (\n            <p style={{color: 'red'}}>\n              Sorry, an error occurred while submitting your message.\n            </p>\n          )}\n        </form>\n      );\n    }\n  }\n\n  class Guestbook extends Component {\n    @provide() static Message = Message;\n\n    @attribute('Message[]') static existingMessages = [];\n\n    @view() static Home() {\n      return (\n        <div style={{maxWidth: '700px', margin: '40px auto'}}>\n          <h1>Guestbook</h1>\n          <this.MessageList />\n          <this.MessageCreator />\n        </div>\n      );\n    }\n\n    @view() static MessageList() {\n      const {Message} = this;\n\n      const [isLoading, loadingError] = useAsyncCall(async () => {\n        this.existingMessages = await Message.find(\n          {},\n          {text: true, createdAt: true},\n          {sort: {createdAt: 'desc'}, limit: 30}\n        );\n      });\n\n      if (isLoading) {\n        return null;\n      }\n\n      if (loadingError) {\n        return (\n          <p style={{color: 'red'}}>\n            Sorry, an error occurred while loading the guestbook’s messages.\n          </p>\n        );\n      }\n\n      return (\n        <div>\n          <h2>All Messages</h2>\n          {this.existingMessages.length > 0 ? (\n            this.existingMessages.map((message) => (\n              <div key={message.id} style={{marginTop: '15px'}}>\n                <message.Viewer />\n              </div>\n            ))\n          ) : (\n            <p>No messages yet.</p>\n          )}\n        </div>\n      );\n    }\n\n    @view() static MessageCreator() {\n      const {Message} = this;\n\n      const [createdMessage, resetCreatedMessage] = useRecomputableMemo(\n        () => new Message()\n      );\n\n      const saveMessage = useCallback(async () => {\n        await createdMessage.save();\n        this.existingMessages = [createdMessage, ...this.existingMessages];\n        resetCreatedMessage();\n      }, [createdMessage]);\n\n      return (\n        <div>\n          <h2>Add a Message</h2>\n          <createdMessage.Form onSubmit={saveMessage} />\n        </div>\n      );\n    }\n  }\n\n  ReactDOM.render(<Guestbook.Home />, document.getElementById('root'));\n}\n\nmain().catch((error) => console.error(error));\n```\n\n```ts\n// TS\n\nimport React, {useCallback} from 'react';\nimport ReactDOM from 'react-dom';\nimport {Component, attribute, provide} from '@layr/component';\nimport {Storable} from '@layr/storable';\nimport {ComponentHTTPClient} from '@layr/component-http-client';\nimport {\n  view,\n  useAsyncCall,\n  useAsyncCallback,\n  useRecomputableMemo\n} from '@layr/react-integration';\n\nimport type {Message as MessageType} from './backend';\n\nasync function main() {\n  const client = new ComponentHTTPClient('http://localhost:3210', {\n    mixins: [Storable]\n  });\n\n  const BackendMessage = (await client.getComponent()) as typeof MessageType;\n\n  class Message extends BackendMessage {\n    @view() Viewer() {\n      return (\n        <div>\n          <small>{this.createdAt.toLocaleString()}</small>\n          <br />\n          <strong>{this.text}</strong>\n        </div>\n      );\n    }\n\n    @view() Form({onSubmit}: {onSubmit: () => Promise<void>}) {\n      const [handleSubmit, isSubmitting, submitError] = useAsyncCallback(\n        async (event) => {\n          event.preventDefault();\n          await onSubmit();\n        }\n      );\n\n      return (\n        <form onSubmit={handleSubmit}>\n          <div>\n            <textarea\n              value={this.text}\n              onChange={(event) => {\n                this.text = event.target.value;\n              }}\n              required\n              style={{width: '100%', height: '80px'}}\n            />\n          </div>\n\n          <p>\n            <button type=\"submit\" disabled={isSubmitting}>\n              Submit\n            </button>\n          </p>\n\n          {submitError && (\n            <p style={{color: 'red'}}>\n              Sorry, an error occurred while submitting your message.\n            </p>\n          )}\n        </form>\n      );\n    }\n  }\n\n  class Guestbook extends Component {\n    @provide() static Message = Message;\n\n    @attribute('Message[]') static existingMessages: Message[] = [];\n\n    @view() static Home() {\n      return (\n        <div style={{maxWidth: '700px', margin: '40px auto'}}>\n          <h1>Guestbook</h1>\n          <this.MessageList />\n          <this.MessageCreator />\n        </div>\n      );\n    }\n\n    @view() static MessageList() {\n      const {Message} = this;\n\n      const [isLoading, loadingError] = useAsyncCall(async () => {\n        this.existingMessages = await Message.find(\n          {},\n          {text: true, createdAt: true},\n          {sort: {createdAt: 'desc'}, limit: 30}\n        );\n      });\n\n      if (isLoading) {\n        return null;\n      }\n\n      if (loadingError) {\n        return (\n          <p style={{color: 'red'}}>\n            Sorry, an error occurred while loading the guestbook’s messages.\n          </p>\n        );\n      }\n\n      return (\n        <div>\n          <h2>All Messages</h2>\n          {this.existingMessages.length > 0 ? (\n            this.existingMessages.map((message) => (\n              <div key={message.id} style={{marginTop: '15px'}}>\n                <message.Viewer />\n              </div>\n            ))\n          ) : (\n            <p>No messages yet.</p>\n          )}\n        </div>\n      );\n    }\n\n    @view() static MessageCreator() {\n      const {Message} = this;\n\n      const [createdMessage, resetCreatedMessage] = useRecomputableMemo(\n        () => new Message()\n      );\n\n      const saveMessage = useCallback(async () => {\n        await createdMessage.save();\n        this.existingMessages = [createdMessage, ...this.existingMessages];\n        resetCreatedMessage();\n      }, [createdMessage]);\n\n      return (\n        <div>\n          <h2>Add a Message</h2>\n          <createdMessage.Form onSubmit={saveMessage} />\n        </div>\n      );\n    }\n  }\n\n  ReactDOM.render(<Guestbook.Home />, document.getElementById('root'));\n}\n\nmain().catch((error) => console.error(error));\n```\n\n> Note that in a real application, we'd spread the code into multiple files. For example, we'd have one file for each class and one more file for the `main()` function. But to keep this guide simple, we've put everything in a single file. See the <!-- <if language=\"js\"> -->[\"CRUD Example App\"](https://github.com/layrjs/crud-example-app-js-webpack)<!-- </if> --><!-- <if language=\"ts\"> -->[\"CRUD Example App\"](https://github.com/layrjs/crud-example-app-ts-webpack)<!-- </if> --> repository for an example of a codebase that is organized into multiple files.\n\nThere is a bunch of code, but if you know a bit of React, it should be pretty easy to read. Compared to the previous [CLI frontend](https://layrjs.com/docs/v1/introduction/data-storage#implementing-the-frontend), we've introduced a few new Layr concepts, and we're going to explore them. But before that, let's start the frontend so you can see how it looks like.\n\n#### Starting the Frontend\n\nStart the frontend in another terminal by invoking the following command:\n\n```sh\nnpx webpack serve --mode=development\n```\n\nThen open [http://localhost:8080/](http://localhost:8080/) in a browser, and you should see the following display:\n\n<p>\n\t<img src=\"https://liaison-blog.s3.dualstack.us-west-2.amazonaws.com/images/guestbook-screen-1.png\" alt=\"Screenshot of the guestbook app with no messages\" style=\"width: 100%; margin-top: .5rem\">\n</p>\n\nSubmit a message, and you should now see something like this:\n\n<p>\n\t<img src=\"https://liaison-blog.s3.dualstack.us-west-2.amazonaws.com/images/guestbook-screen-2.png\" alt=\"Screenshot of the guestbook app with one message\" style=\"width: 100%; margin-top: .5rem\">\n</p>\n\nJust to make sure that your message is stored into the backend, refresh the browser, and you should see that your message is indeed still there.\n\n#### Making Sense of the Frontend\n\nThe frontend is made of two components:\n\n- `Message` inherits from the backend's `Message` component thanks to the cross-layer component inheritance ability.\n- `Guestbook` defines a new [`Component`](https://layrjs.com/docs/v1/reference/component).\n\n##### `Message` Component\n\nLayr embraces the object-oriented approach in all aspects of an application and allows you to organize your code in a way that is as cohesive as possible. Traditionally, domain models and UI views are completely separated, but we think that [another way is possible](https://layrjs.com/blog/articles/Do-We-Really-Need-to-Separate-the-Model-from-the-UI-9wogqr). So, the \"Layr way\" is to co-locate a model and its views in the same place — a Layr [`Component`](https://layrjs.com/docs/v1/reference/component).\n\nThe `Message` component is composed (besides the attributes and methods that are inherited from the backend) of two views:\n\n- `Viewer()` represents a view to display a message.\n- `Form()` represents a form to edit a message.\n\nBoth views are just methods that return some [React elements](https://reactjs.org/docs/rendering-elements.html) and they are prefixed with the [`@view()`](https://layrjs.com/docs/v1/reference/react-integration#view-decorator) decorator that essentially does two things:\n\n- First, it binds a \"view method\" to a specific component, so when the method is executed by React (via, for example, a reference included in a [JSX expression](https://reactjs.org/docs/introducing-jsx.html)), it has access to the bound component through `this`.\n- Second, it observes the attributes of the bound component, so when the value of an attribute changes, the view is automatically re-rendered.\n\n###### `Viewer()` View\n\nThe `Viewer()` view is quite self-explanatory. It just wraps some message's attributes in some HTML elements.\n\n###### `Form()` View\n\nThe `Form()` view returns a `form` element and implements a bit of logic so the user can interact with to form.\n\nWe use the [`useAsyncCallback()`](https://layrjs.com/docs/v1/reference/react-integration#use-async-callback-react-hook) hook, which is a handy way to track the execution of an asynchronous function. In our case, it is used to track the form submission. So when the form is being submitted, the \"Submit\" button is disabled, and if the submission fails, an error is displayed.\n\nThe rest of the method is just regular React code.\n\n##### `Guestbook` Component\n\nThe `Guestbook` component is the core of the application. It contains an attribute (`existingMessages`) to keep track of the displayed messages, and it provides a couple of views (`Home()`, `MessageList()`, and `MessageCreator()`) to display the whole application.\n\nAt the very beginning of the class, the [`@provide()`](https://layrjs.com/docs/v1/reference/component#provide-decorator) decorator is used to make the `Guestbook` component aware of the `Message` component. Doing so allows us to specify the `'Message'` type as a parameter of the [`@attribute()`](https://layrjs.com/docs/v1/reference/component#attribute-decorator) decorator to define the `existingMessages` attribute.\n\nAnother benefit of using the [`@provide()`](https://layrjs.com/docs/v1/reference/component#provide-decorator) decorator is that we can access the `Message` component through `this.Message` from any `Guestbook`'s class method (or through `this.constructor.Message` from any instance method), and doing so brings a lot of advantages. First, it is a good practice to reduce the direct references to an external component as much as possible. Second, accessing a component through a reference that is managed by the [`@provide()`](https://layrjs.com/docs/v1/reference/component#provide-decorator) decorator enables some unique Layr's features such as [component forking](https://layrjs.com/docs/v1/reference/component?language=js#forking).\n\n###### `Home()` View\n\nThe `Home()` view is pretty straightforward. It just renders other views (`MessageList()` and `MessageCreator()`) in a minimal layout.\n\n###### `MessageList()` View\n\nThe `MessageList()` view is in charge of loading some messages from the backend and rendering them.\n\nWe use the [`useAsyncCall()`](https://layrjs.com/docs/v1/reference/react-integration#use-async-call-react-hook) hook to track the loading of the messages. When the messages are being loaded, we return `null`, and if the loading fails we render an error message. Otherwise, we render the loaded messages using their `Viewer()` view.\n\n###### `MessageCreator()` View\n\nThe `MessageCreator()` view allows the user to create a new message and save it into the backend.\n\nThe `createdMessage` variable is initialized using the [`useRecomputableMemo()`](https://layrjs.com/docs/v1/reference/react-integration#use-recomputable-memo-react-hook) hook, which plays the same role as the React [useMemo()](https://reactjs.org/docs/hooks-reference.html#usememo) hook, but in addition to a memoized value, we get a function that we can call anytime to recompute the memoized value.\n\nThe `saveMessage()` callback is simply defined using the React [useCallback()](https://reactjs.org/docs/hooks-reference.html#usecallback) hook, and it is later passed to the `createdMessage`'s `Form()` view so the message can be saved when the user clicks the \"Submit\" button.\n\nOnce the `createdMessage` is saved, we add it to the `existingMessages` so it can be instantly displayed with all other messages.\n\nLastly, we reset the `createdMessage` by calling `resetCreatedMessage()`.\n\n#### Wrapping Up\n\nWe've built a simple web app with a frontend, a backend, and a database. And thanks to Layr, we were able to free ourselves from several boring tasks that we usually encounter:\n\n- We didn't have to build a web API to connect the frontend and the backend.\n- To interact with the database, there was no need to add an ORM or a query builder.\n- To build the frontend, we didn't have to bother with a state manager.\n\nIf you are a seasoned React developer or a functional programming advocate, you might be a little surprised by the way the frontend was implemented. But please don't judge too quickly, give Layr a try, and hopefully, you'll see how your projects could be dramatically simplified with the object-oriented approach that Layr is enabling.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/attribute-4P9dTivZ9HJ4Flr4Y8cTKv.immutable.md",
    "content": "### Attribute <badge type=\"primary\">class</badge> {#attribute-class}\n\n*Inherits from [`Property`](https://layrjs.com/docs/v1/reference/property) and [`Observable`](https://layrjs.com/docs/v1/reference/observable#observable-class).*\n\nAn `Attribute` represents an attribute of a [Component](https://layrjs.com/docs/v1/reference/component) class, prototype, or instance. It plays the role of a regular JavaScript object attribute, but brings some extra features such as runtime type checking, validation, serialization, or observability.\n\n#### Usage\n\nTypically, you create an `Attribute` and associate it to a component by using the [`@attribute()`](https://layrjs.com/docs/v1/reference/component#attribute-decorator) decorator.\n\nFor example, here is how you would define a `Movie` class with some attributes:\n\n```\n// JS\n\nimport {Component, attribute, validators} from '@layr/component';\n\nconst {minLength} = validators;\n\nclass Movie extends Component {\n  // Optional 'string' class attribute\n  @attribute('string?') static customName;\n\n  // Required 'string' instance attribute\n  @attribute('string') title;\n\n  // Optional 'string' instance attribute with a validator and a default value\n  @attribute('string?', {validators: [minLength(16)]}) summary = '';\n}\n```\n\n```\n// TS\n\nimport {Component, attribute, validators} from '@layr/component';\n\nconst {minLength} = validators;\n\nclass Movie extends Component {\n  // Optional 'string' class attribute\n  @attribute('string?') static customName?: string;\n\n  // Required 'string' instance attribute\n  @attribute('string') title!: string;\n\n  // Optional 'string' instance attribute with a validator and a default value\n  @attribute('string?', {validators: [minLength(16)]}) summary? = '';\n}\n```\n\nThen you can access the attributes like you would normally do with regular JavaScript objects:\n\n```\nMovie.customName = 'Film';\nMovie.customName; // => 'Film'\n\nconst movie = new Movie({title: 'Inception'});\nmovie.title; // => 'Inception'\nmovie.title = 'Inception 2';\nmovie.title; // => 'Inception 2'\nmovie.summary; // => '' (default value)\n```\n\nAnd you can take profit of some extra features:\n\n```\n// Runtime type checking\nmovie.title = 123; // Error\nmovie.title = undefined; // Error\n\n// Validation\nmovie.summary = undefined;\nmovie.isValid(); // => true (movie.summary is optional)\nmovie.summary = 'A nice movie.';\nmovie.isValid(); // => false (movie.summary is too short)\nmovie.summary = 'An awesome movie.'\nmovie.isValid(); // => true\n\n// Serialization\nmovie.serialize();\n// => {__component: 'Movie', title: 'Inception 2', summary: 'An awesome movie.'}\n```\n\n#### Creation\n\n##### `new Attribute(name, parent, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates an instance of [`Attribute`](https://layrjs.com/docs/v1/reference/attribute). Typically, instead of using this constructor, you would rather use the [`@attribute()`](https://layrjs.com/docs/v1/reference/component#attribute-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the attribute.\n* `parent`: The component class, prototype, or instance that owns the attribute.\n* `options`:\n  * `valueType`: A string specifying the [type of values](https://layrjs.com/docs/v1/reference/value-type#supported-types) the attribute can store (default: `'any'`).\n  * `value`: The initial value of the attribute (usable for class attributes only).\n  * `default`: The default value (or a function returning the default value) of the attribute (usable for instance attributes only).\n  * `validators`: An array of [validators](https://layrjs.com/docs/v1/reference/validator) for the value of the attribute.\n  * `items`:\n    * `validators`: An array of [validators](https://layrjs.com/docs/v1/reference/validator) for the items of an array attribute.\n  * `getter`: A getter function for getting the value of the attribute. Plays the same role as a regular [JavaScript getter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get).\n  * `setter`: A setter function for setting the value of the attribute. Plays the same role as a regular [JavaScript setter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set).\n  * `exposure`: A [`PropertyExposure`](https://layrjs.com/docs/v1/reference/property#property-exposure-type) object specifying how the attribute should be exposed to remote access.\n\n**Returns:**\n\nThe [`Attribute`](https://layrjs.com/docs/v1/reference/attribute) instance that was created.\n\n**Example:**\n\n```\nimport {Component, Attribute} from '@layr/component';\n\nclass Movie extends Component {}\n\nconst title = new Attribute('title', Movie.prototype, {valueType: 'string'});\n\ntitle.getName(); // => 'title'\ntitle.getParent(); // => Movie.prototype\ntitle.getValueType().toString(); // => 'string'\n```\n\n#### Property Methods\n\nSee the methods that are inherited from the [`Property`](https://layrjs.com/docs/v1/reference/property#basic-methods) class.\n\n#### Value Type\n\n##### `getValueType()` <badge type=\"secondary-outline\">instance method</badge> {#get-value-type-instance-method}\n\nReturns the type of values the attribute can store.\n\n**Returns:**\n\nA [ValueType](https://layrjs.com/docs/v1/reference/value-type) instance.\n\n**Example:**\n\n```\nconst title = Movie.prototype.getAttribute('title');\ntitle.getValueType(); // => A ValueType instance\ntitle.getValueType().toString(); // => 'string'\ntitle.getValueType().isOptional(); // => false\n```\n\n#### Value\n\n##### `getValue([options])` <badge type=\"secondary-outline\">instance method</badge> {#get-value-instance-method}\n\nReturns the current value of the attribute.\n\n**Parameters:**\n\n* `options`:\n  * `throwIfUnset`: A boolean specifying whether the method should throw an error if the value is not set (default: `true`). If `false` is specified and the value is not set, the method returns `undefined`.\n\n**Returns:**\n\nA value of the type handled by the attribute.\n\n**Example:**\n\n```\nconst title = movie.getAttribute('title');\ntitle.getValue(); // => 'Inception'\ntitle.unsetValue();\ntitle.getValue(); // => Error\ntitle.getValue({throwIfUnset: false}); // => undefined\n```\n\n##### `setValue(value, [options])` <badge type=\"secondary-outline\">instance method</badge> {#set-value-instance-method}\n\nSets the value of the attribute. If the type of the value doesn't match the expected type, an error is thrown.\n\nWhen the attribute's value changes, the observers of the attribute are automatically executed, and the observers of the parent component are executed as well.\n\n**Parameters:**\n\n* `value`: The value to be set.\n* `options`:\n  * `source`: A number specifying the [source of the value](https://layrjs.com/docs/v1/reference/attribute#value-source-type) (default: `0`).\n\n**Example:**\n\n```\nconst title = movie.getAttribute('title');\ntitle.setValue('Inception 2');\ntitle.setValue(123); // => Error\n```\n\n##### `unsetValue()` <badge type=\"secondary-outline\">instance method</badge> {#unset-value-instance-method}\n\nUnsets the value of the attribute. If the value is already unset, nothing happens.\n\n**Example:**\n\n```\nconst title = movie.getAttribute('title');\ntitle.isSet(); // => true\ntitle.unsetValue();\ntitle.isSet(); // => false\n```\n\n##### `isSet()` <badge type=\"secondary-outline\">instance method</badge> {#is-set-instance-method}\n\nReturns whether the value of the attribute is set or not.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nconst title = movie.getAttribute('title');\ntitle.isSet(); // => true\ntitle.unsetValue();\ntitle.isSet(); // => false\n```\n\n#### Value Source\n\n##### `getValueSource()` <badge type=\"secondary-outline\">instance method</badge> {#get-value-source-instance-method}\n\nReturns the source of the value of the attribute.\n\n**Returns:**\n\nA [`ValueSource`](https://layrjs.com/docs/v1/reference/attribute#value-source-type) number.\n\n**Example:**\n\n```\nconst title = movie.getAttribute('title');\ntitle.getValueSource(); // => 0 (the value was set locally)\n```\n\n##### `setValueSource(source)` <badge type=\"secondary-outline\">instance method</badge> {#set-value-source-instance-method}\n\nSets the source of the value of the attribute.\n\n**Parameters:**\n\n* `source`: A [`ValueSource`](https://layrjs.com/docs/v1/reference/attribute#value-source-type) number.\n\n**Example:**\n\n```\nconst title = movie.getAttribute('title');\ntitle.setValueSource(0); // The value was set locally\ntitle.setValueSource(1); // The value came from an upper layer (e.g., the backend)\ntitle.setValueSource(-1); // The value came from a lower layer (e.g., the frontend)\n```\n\n##### `ValueSource` <badge type=\"primary-outline\">type</badge> {#value-source-type}\n\nA number representing the source of a value.\n\nCurrently, three values are supported:\n\n* `0`: The value comes from the current layer (i.e., the current runtime environment).\n* `1`: The value comes from an upper layer (e.g., the backend).\n* `-1`: The value comes from a lower layer (e.g., the frontend).\n```\n\n#### Default Value\n\n##### `getDefault()` <badge type=\"secondary-outline\">instance method</badge> {#get-default-instance-method}\n\nReturns the default value of the attribute as specified when the attribute was created.\n\n**Returns:**\n\nA value or a function returning a value.\n\n**Example:**\n\n```\nconst summary = movie.getAttribute('summary');\nsummary.getDefault(); // => function () { return ''; }\n```\n\n##### `evaluateDefault()` <badge type=\"secondary-outline\">instance method</badge> {#evaluate-default-instance-method}\n\nEvaluate the default value of the attribute. If the default value is a function, the function is called (with the attribute's parent as `this` context), and the result is returned. Otherwise, the default value is returned as is.\n\n**Returns:**\n\nA value of any type.\n\n**Example:**\n\n```\nconst summary = movie.getAttribute('summary');\nsummary.evaluateDefault(); // ''\n```\n\n#### Validation\n\n##### `validate([attributeSelector])` <badge type=\"secondary-outline\">instance method</badge> {#validate-instance-method}\n\nValidates the value of the attribute. If the value doesn't pass the validation, an error is thrown. The error is a JavaScript `Error` instance with a `failedValidators` custom attribute which contains the result of the [`runValidators()`](https://layrjs.com/docs/v1/reference/attribute#run-validators-instance-method) method.\n\n**Parameters:**\n\n* `attributeSelector`: In case the value of the attribute is a component, your can pass an [`AttributeSelector`](https://layrjs.com/docs/v1/reference/attribute-selector) specifying the component's attributes to be validated (default: `true`, which means that all the component's attributes will be validated).\n\n**Example:**\n\n```\n// JS\n\nimport {Component, attribute, validators} from '@layr/component';\n\nconst {notEmpty} = validators;\n\nclass Movie extends Component {\n  @attribute('string', {validators: [notEmpty()]}) title;\n}\n\nconst movie = new Movie({title: 'Inception'});\nconst title = movie.getAttribute('title');\n\ntitle.getValue(); // => 'Inception'\ntitle.validate(); // All good!\ntitle.setValue('');\ntitle.validate(); // => Error {failedValidators: [{validator: ..., path: ''}]}\n```\n```\n// TS\n\nimport {Component, attribute, validators} from '@layr/component';\n\nconst {notEmpty} = validators;\n\nclass Movie extends Component {\n  @attribute('string', {validators: [notEmpty()]}) title!: string;\n}\n\nconst movie = new Movie({title: 'Inception'});\nconst title = movie.getAttribute('title');\n\ntitle.getValue(); // => 'Inception'\ntitle.validate(); // All good!\ntitle.setValue('');\ntitle.validate(); // => Error {failedValidators: [{validator: ..., path: ''}]}\n```\n\n##### `isValid([attributeSelector])` <badge type=\"secondary-outline\">instance method</badge> {#is-valid-instance-method}\n\nReturns whether the value of the attribute is valid.\n\n**Parameters:**\n\n* `attributeSelector`: In case the value of the attribute is a component, your can pass an [`AttributeSelector`](https://layrjs.com/docs/v1/reference/attribute-selector) specifying the component's attributes to be validated (default: `true`, which means that all the component's attributes will be validated).\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\n// See the `title` definition in the `validate()` example\n\ntitle.getValue(); // => 'Inception'\ntitle.isValid(); // => true\ntitle.setValue('');\ntitle.isValid(); // => false\n```\n\n##### `runValidators([attributeSelector])` <badge type=\"secondary-outline\">instance method</badge> {#run-validators-instance-method}\n\nRuns the validators with the value of the attribute.\n\n**Parameters:**\n\n* `attributeSelector`: In case the value of the attribute is a component, your can pass an [`AttributeSelector`](https://layrjs.com/docs/v1/reference/attribute-selector) specifying the component's attributes to be validated (default: `true`, which means that all the component's attributes will be validated).\n\n**Returns:**\n\nAn array containing the validators that have failed. Each item is a plain object composed of a `validator` (a [`Validator`](https://layrjs.com/docs/v1/reference/validator) instance) and a `path` (a string representing the path of the attribute containing the validator that has failed).\n\n**Example:**\n\n```\n// See the `title` definition in the `validate()` example\n\ntitle.getValue(); // => 'Inception'\ntitle.runValidators(); // => []\ntitle.setValue('');\ntitle.runValidators(); // => [{validator: ..., path: ''}]\n```\n\n#### Observability\n\nSee the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v1/reference/observable#observable-class) class.\n\n#### Utilities\n\n##### `isAttributeClass(value)` <badge type=\"tertiary-outline\">function</badge> {#is-attribute-class-function}\n\nReturns whether the specified value is an `Attribute` class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isAttributeInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-attribute-instance-function}\n\nReturns whether the specified value is an `Attribute` instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/attribute-selector-7FZWQpwLR7jpUHoBpvO9Et.immutable.md",
    "content": "### AttributeSelector <badge type=\"primary-outline\">type</badge> {#attribute-selector-type}\n\nAn `AttributeSelector` allows you to select some attributes of a component.\n\nThe simplest `AttributeSelector` is `true`, which means that all the attributes are selected.\n\nAnother possible `AttributeSelector` is `false`, which means that no attributes are selected.\n\nTo select some specific attributes, you can use a plain object where:\n\n* The keys are the name of the attributes you want to select.\n* The values are a boolean or a nested object to select some attributes of a nested component.\n\n**Examples:**\n\n```\n// Selects all the attributes\ntrue\n\n// Excludes all the attributes\nfalse\n\n// Selects `title`\n{title: true}\n\n// Selects also `title` (`summary` is not selected)\n{title: true, summary: false}\n\n// Selects `title` and `summary`\n{title: true, summary: true}\n\n// Selects `title`, `movieDetails.duration`, and `movieDetails.aspectRatio`\n{\n  title: true,\n  movieDetails: {\n    duration: true,\n    aspectRatio: true\n  }\n}\n```\n\n#### Functions\n\n##### `createAttributeSelectorFromNames(names)` <badge type=\"tertiary-outline\">function</badge> {#create-attribute-selector-from-names-function}\n\nCreates an `AttributeSelector` from the specified names.\n\n**Parameters:**\n\n* `names`: An array of strings.\n\n**Returns:**\n\nAn `AttributeSelector`.\n\n**Example:**\n\n```\ncreateAttributeSelectorFromNames(['title', 'summary']);\n// => {title: true, summary: true}\n```\n\n##### `createAttributeSelectorFromAttributes(attributes)` <badge type=\"tertiary-outline\">function</badge> {#create-attribute-selector-from-attributes-function}\n\nCreates an `AttributeSelector` from an attribute iterator.\n\n**Parameters:**\n\n* `attributes`: An [`Attribute`](https://layrjs.com/docs/v1/reference/attribute) iterator.\n\n**Returns:**\n\nAn `AttributeSelector`.\n\n**Example:**\n\n```\ncreateAttributeSelectorFromAttributes(Movie.prototype.getAttributes());\n// => {title: true, summary: true, movieDetails: true}\n```\n\n##### `getFromAttributeSelector(attributeSelector, name)` <badge type=\"tertiary-outline\">function</badge> {#get-from-attribute-selector-function}\n\nGets an entry of an `AttributeSelector`.\n\n**Parameters:**\n\n* `attributeSelector`: An `AttributeSelector`.\n* `name`: The name of the entry to get.\n\n**Returns:**\n\nAn `AttributeSelector`.\n\n**Example:**\n\n```\ngetFromAttributeSelector(true, 'title');\n// => true\n\ngetFromAttributeSelector(false, 'title');\n// => false\n\ngetFromAttributeSelector({title: true}, 'title');\n// => true\n\ngetFromAttributeSelector({title: true}, 'summary');\n// => false\n\ngetFromAttributeSelector({movieDetails: {duration: true}}, 'movieDetails');\n// => {duration: true}\n```\n\n##### `setWithinAttributeSelector(attributeSelector, name, subattributeSelector)` <badge type=\"tertiary-outline\">function</badge> {#set-within-attribute-selector-function}\n\nReturns an `AttributeSelector` where an entry of the specified `AttributeSelector` is set with another `AttributeSelector`.\n\n**Parameters:**\n\n* `attributeSelector`: An `AttributeSelector`.\n* `name`: The name of the entry to set.\n* `subattributeSelector`: Another `AttributeSelector`.\n\n**Returns:**\n\nA new `AttributeSelector`.\n\n**Example:**\n\n```\nsetWithinAttributeSelector({title: true}, 'summary', true);\n// => {title: true, summary: true}\n\nsetWithinAttributeSelector({title: true}, 'summary', false);\n// => {title: true}\n\nsetWithinAttributeSelector({title: true, summary: true}, 'summary', false);\n// => {title: true}\n\nsetWithinAttributeSelector({title: true}, 'movieDetails', {duration: true});\n// => {title: true, movieDetails: {duration: true}}\n```\n\n##### `cloneAttributeSelector(attributeSelector)` <badge type=\"tertiary-outline\">function</badge> {#clone-attribute-selector-function}\n\nClones an `AttributeSelector`.\n\n**Parameters:**\n\n* `attributeSelector`: An `AttributeSelector`.\n\n**Returns:**\n\nA new `AttributeSelector`.\n\n**Example:**\n\n```\ncloneAttributeSelector(true);\n// => true\n\ncloneAttributeSelector(false);\n// => false\n\ncloneAttributeSelector({title: true, movieDetails: {duration: true});\n// => {title: true, movieDetails: {duration: true}\n```\n\n##### `attributeSelectorsAreEqual(attributeSelector, otherAttributeSelector)` <badge type=\"tertiary-outline\">function</badge> {#attribute-selectors-are-equal-function}\n\nReturns whether an `AttributeSelector` is equal to another `AttributeSelector`.\n\n**Parameters:**\n\n* `attributeSelector`: An `AttributeSelector`.\n* `otherAttributeSelector`: Another `AttributeSelector`.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nattributeSelectorsAreEqual({title: true}, {title: true});\n// => true\n\nattributeSelectorsAreEqual({title: true, summary: false}, {title: true});\n// => true\n\nattributeSelectorsAreEqual({title: true}, {summary: true});\n// => false\n```\n\n##### `attributeSelectorIncludes(attributeSelector, otherAttributeSelector)` <badge type=\"tertiary-outline\">function</badge> {#attribute-selector-includes-function}\n\nReturns whether an `AttributeSelector` includes another `AttributeSelector`.\n\n**Parameters:**\n\n* `attributeSelector`: An `AttributeSelector`.\n* `otherAttributeSelector`: Another `AttributeSelector`.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nattributeSelectorIncludes({title: true}, {title: true});\n// => true\n\nattributeSelectorIncludes({title: true, summary: true}, {title: true});\n// => true\n\nattributeSelectorIncludes({title: true}, {summary: true});\n// => false\n```\n\n##### `mergeAttributeSelectors(attributeSelector, otherAttributeSelector)` <badge type=\"tertiary-outline\">function</badge> {#merge-attribute-selectors-function}\n\nReturns an `AttributeSelector` which is the result of merging an `AttributeSelector` with another `AttributeSelector`.\n\n**Parameters:**\n\n* `attributeSelector`: An `AttributeSelector`.\n* `otherAttributeSelector`: Another `AttributeSelector`.\n\n**Returns:**\n\nA new `AttributeSelector`.\n\n**Example:**\n\n```\nmergeAttributeSelectors({title: true}, {title: true});\n// => {title: true}\n\nmergeAttributeSelectors({title: true}, {summary: true});\n// => {title: true, summary: true}\n\nmergeAttributeSelectors({title: true, summary: true}, {summary: false});\n// => {title: true}\n```\n\n##### `intersectAttributeSelectors(attributeSelector, otherAttributeSelector)` <badge type=\"tertiary-outline\">function</badge> {#intersect-attribute-selectors-function}\n\nReturns an `AttributeSelector` which is the result of the intersection of an `AttributeSelector` with another `AttributeSelector`.\n\n**Parameters:**\n\n* `attributeSelector`: An `AttributeSelector`.\n* `otherAttributeSelector`: Another `AttributeSelector`.\n\n**Returns:**\n\nA new `AttributeSelector`.\n\n**Example:**\n\n```\nintersectAttributeSelectors({title: true, summary: true}, {title: true});\n// => {title: true}\n\nintersectAttributeSelectors({title: true}, {summary: true});\n// => {}\n```\n\n##### `removeFromAttributeSelector(attributeSelector, otherAttributeSelector)` <badge type=\"tertiary-outline\">function</badge> {#remove-from-attribute-selector-function}\n\nReturns an `AttributeSelector` which is the result of removing an `AttributeSelector` from another `AttributeSelector`.\n\n**Parameters:**\n\n* `attributeSelector`: An `AttributeSelector`.\n* `otherAttributeSelector`: Another `AttributeSelector`.\n\n**Returns:**\n\nA new `AttributeSelector`.\n\n**Example:**\n\n```\nremoveFromAttributeSelector({title: true, summary: true}, {summary: true});\n// => {title: true}\n\nremoveFromAttributeSelector({title: true}, {title: true});\n// => {}\n```\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/aws-integration-3iPKbtjOS0uDgsZKk6Q0Kp.immutable.md",
    "content": "### aws-integration <badge type=\"primary\">module</badge> {#aws-integration-module}\n\nAllows you to host a [component server](https://layrjs.com/docs/v1/reference/component-server) in [AWS Lambda](https://aws.amazon.com/lambda/).\n\n#### Functions\n\n##### `createAWSLambdaHandlerForComponentServer(componentServer)` <badge type=\"tertiary-outline\">function</badge> {#create-aws-lambda-handler-for-component-server-function}\n\nCreates an [AWS Lambda function handler](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html) for the specified [component server](https://layrjs.com/docs/v1/reference/component-server).\n\nThe created handler can be hosted in [AWS Lambda](https://aws.amazon.com/lambda/) and consumed by [AWS API Gateway](https://aws.amazon.com/api-gateway/) through an [HTTP API](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api.html).\n\n**Parameters:**\n\n* `componentServer`: A [`ComponentServer`](https://layrjs.com/docs/v1/reference/component-server) instance.\n\n**Returns:**\n\nAn [AWS Lambda function handler](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html).\n\n**Example:**\n\n```\nimport {Component, attribute, expose} from '@layr/component';\nimport {ComponentServer} from '@layr/component-server';\nimport {createAWSLambdaHandlerForComponentServer} from '@layr/aws-integration';\n\nclass Movie extends Component {\n  @expose({get: true, set: true}) @attribute('string') title = '';\n}\n\nconst server = new ComponentServer(Movie);\n\nconst handler = createAWSLambdaHandlerForComponentServer(server);\n\nexport {handler};\n```\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/browser-router-6EIyPCgoPYsWmDmpyKMhce.immutable.md",
    "content": "### BrowserRouter <badge type=\"primary\">class</badge> {#browser-router-class}\n\n*Inherits from [`Router`](https://layrjs.com/docs/v1/reference/router).*\n\nA [`Router`](https://layrjs.com/docs/v1/reference/router) relying on the browser's [History API](https://developer.mozilla.org/en-US/docs/Web/API/History) to determine the current [route](https://layrjs.com/docs/v1/reference/route).\n\n#### Usage\n\nIf you are using [React](https://reactjs.org/), the easiest way to set up a `BrowserRouter` in your application is to use the [`useBrowserRouter()`](https://layrjs.com/docs/v1/reference/react-integration#use-browser-router-react-hook) hook that is provided by the `@layr/react-integration` package.\n\n> See the [\"Bringing Some Routes\"](https://layrjs.com/docs/v1/introduction/routing) guide for a comprehensive example using the `useBrowserRouter()` hook.\n\nOtherwise, you can create a `BrowserRouter` instance manually, register some [routable components](https://layrjs.com/docs/v1/reference/routable#routable-component-class) into it, and observe it to automatically display the current route when the user navigates.\n\n**Example:**\n\n```\nimport {Component} from '@layr/component';\nimport {Routable, route} from '@layr/routable';\nimport {BrowserRouter} from '@layr/browser-router';\n\nclass Frontend extends Routable(Component) {\n  @route('/') static Home() {\n    // Return the content of the home page...\n    return 'Home Page';\n  }\n\n  @route('/about') static About() {\n    // Return the content of the about page...\n    return 'About Page';\n  }\n}\n\nconst router = new BrowserRouter();\n\nrouter.registerRoutable(Frontend);\n\nrouter.addObserver(() => {\n  const result = router.callCurrentRoute();\n  // Display the result in the browser...\n});\n```\n\n#### Creation\n\n##### `new BrowserRouter()` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates a [`BrowserRouter`](https://layrjs.com/docs/v1/reference/browser-router).\n\n**Returns:**\n\nThe [`BrowserRouter`](https://layrjs.com/docs/v1/reference/browser-router) instance that was created.\n\n#### Component Registration\n\nSee the methods that are inherited from the [`Router`](https://layrjs.com/docs/v1/reference/router#component-registration) class.\n\n#### Routes\n\nSee the methods that are inherited from the [`Router`](https://layrjs.com/docs/v1/reference/router#routes) class.\n\n#### Current Location\n\nSee the methods that are inherited from the [`Router`](https://layrjs.com/docs/v1/reference/router#current-location) class.\n\n#### Navigation\n\nSee the methods that are inherited from the [`Router`](https://layrjs.com/docs/v1/reference/router#navigation) class.\n\n#### Link Rendering\n\n##### `Link(props)` <badge type=\"secondary-outline\">instance method</badge> {#link-instance-method}\n\nRenders a link that is managed by the router.\n\nThis method is only available when you create your router by using the [`useBrowserRouter()`](https://layrjs.com/docs/v1/reference/react-integration#use-browser-router-react-hook) React hook.\n\nNote that instead of using this method, you can use the handy `Link()` shortcut function that you get when you define a route with the [`@route()`](https://layrjs.com/docs/v1/reference/routable#route-decorator) decorator.\n\n**Parameters:**\n\n* `props`:\n  * `to`: A string representing the target URL of the link.\n  * `className`: A [`className`](https://reactjs.org/docs/dom-elements.html#classname) attribute to apply to the rendered link.\n  * `activeClassName`: An additional [`className`](https://reactjs.org/docs/dom-elements.html#classname) attribute to apply to the rendered link when the URL of the current router's route is the same as the target URL of the link.\n  * `style`: A [`style`](https://reactjs.org/docs/dom-elements.html#style) attribute to apply to the rendered link.\n  * `activeStyle`: An additional [`style`](https://reactjs.org/docs/dom-elements.html#style) attribute to apply to the rendered link when the URL of the current router's route is the same as the target URL of the link.\n\n**Returns:**\n\nAn `<a>` React element.\n\n**Example:**\n\n```\nclass Frontend extends Routable(Component) {\n   @route('/') @view static Home() {\n     const router = this.getRouter();\n     return <router.Link to=\"/about-us\">About Us</router.Link>;\n\n     // Same as above, but in a more idiomatic way:\n     return <this.AboutUs.Link>About Us</this.AboutUs.Link>;\n   }\n\n   @route('/about-us') @view static AboutUs() {\n     return <div>Here is everything about us.<div>;\n   }\n}\n```\n\n#### Observability\n\nSee the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v1/reference/observable#observable-class) class.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/component-6LvsJL2MA9RN6hmT0bBacd.immutable.md",
    "content": "### Component <badge type=\"primary\">class</badge> {#component-class}\n\n*Inherits from [`Observable`](https://layrjs.com/docs/v1/reference/observable#observable-class).*\n\nA component is an elementary building block allowing you to define your data models and implement the business logic of your application. Typically, an application is composed of several components that are connected to each other by using the [`@provide()`](https://layrjs.com/docs/v1/reference/component#provide-decorator) and [`@consume()`](https://layrjs.com/docs/v1/reference/component#consume-decorator) decorators.\n\n#### Usage\n\nJust extend the `Component` class to define a component with some attributes and methods that are specific to your application.\n\nFor example, a `Movie` component with a `title` attribute and a `play()` method could be defined as follows:\n\n```\n// JS\n\nimport {Component} from '@layr/component';\n\nclass Movie extends Component {\n  @attribute('string') title;\n\n  @method() play() {\n    console.log(`Playing '${this.title}...'`);\n  }\n}\n```\n\n```\n// TS\n\nimport {Component} from '@layr/component';\n\nclass Movie extends Component {\n  @attribute('string') title!: string;\n\n  @method() play() {\n    console.log(`Playing '${this.title}...'`);\n  }\n}\n```\n\nThe [`@attribute()`](https://layrjs.com/docs/v1/reference/component#attribute-decorator) and [`@method()`](https://layrjs.com/docs/v1/reference/component#method-decorator) decorators allows you to get the full power of Layr, such as attribute type checking, or remote method invocation.\n\nOnce you have defined a component, you can use it as any JavaScript class:\n\n```\nconst movie = new Movie({title: 'Inception'});\n\nmovie.play(); // => 'Playing Inception...'\n```\n\n#### Nesting Components\n\nComponents can be nested either by embedding or by referencing.\n\n##### Embedding Components\n\nUse the [`EmbeddedComponent`](https://layrjs.com/docs/v1/reference/embedded-component) class to embed a component into another component. An embedded component is strongly attached to the parent component that owns it, and it cannot \"live\" by itself like a regular component.\n\nHere are some characteristics of an embedded component:\n\n- An embedded component has one parent only, and therefore cannot be embedded in more than one component.\n- When the parent of an embedded component is [validated](https://layrjs.com/docs/v1/reference/validator), the embedded component is validated as well.\n- When the parent of an embedded component is loaded, saved, or deleted (using a [`StorableComponent`](https://layrjs.com/docs/v1/reference/storable#storage-operations) method), the embedded component is loaded, saved, or deleted as well.\n\nSee the [`EmbeddedComponent`](https://layrjs.com/docs/v1/reference/embedded-component) class for an example of use.\n\n##### Referencing Components\n\nAny non-embedded component can be referenced by another component. Contrary to an embedded component, a referenced component is an independent entity that can \"live\" by itself. So a referenced component behaves like a regular JavaScript object that is referenced by another object.\n\nHere are some characteristics of a referenced component:\n\n- A referenced component can be referenced by any number of components.\n- When a component holding a reference to another component is [validated](https://layrjs.com/docs/v1/reference/validator), the referenced component is considered as an independent entity, and is therefore not automatically validated.\n- When a component holding a reference to another component is loaded, saved, or deleted (using a [`StorableComponent`](https://layrjs.com/docs/v1/reference/storable#storage-operations) method), the referenced component can be loaded in the same operation, but it has to be saved or deleted independently.\n\nFor example, let's say we have a `Director` component defined as follows:\n\n```\n// JS\n\nclass Director extends Component {\n  @attribute('string') fullName;\n}\n```\n\n```\n// TS\n\nclass Director extends Component {\n  @attribute('string') fullName!: string;\n}\n```\n\nNext, we can add an attribute to the `Movie` component to store a reference to a `Director`:\n\n```\n// JS\n\nclass Movie extends Component {\n  @provide() static Director = Director;\n\n  // ...\n\n  @attribute('Director') director;\n}\n```\n\n```\n// TS\n\nclass Movie extends Component {\n  @provide() static Director = Director;\n\n  // ...\n\n  @attribute('Director') director!: Director;\n}\n```\n\n> Note that to be able to specify the `'Director'` type for the `director` attribute, you first have to provide the `Director` component to the `Movie` component by using the [`@provide()`](https://layrjs.com/docs/v1/reference/component#provide-decorator) decorator.\n\n Then, to create a `Movie` with a `Director`, we can do something like:\n\n```\nconst movie = new Movie({\n  title: 'Inception',\n  director: new Movie.Director({fullName: 'Christopher Nolan'})\n});\n\nmovie.title; // => 'Inception'\nmovie.director.fullName; // => 'Christopher Nolan'\n```\n\n#### Creation\n\n##### `new Component([object])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates an instance of a component class.\n\n**Parameters:**\n\n* `object`: An optional object specifying the value of the component attributes.\n\n**Returns:**\n\nThe component instance that was created.\n\n**Example:**\n\n```\n// JS\n\nimport {Component, attribute} from '@layr/component';\n\nclass Movie extends Component {\n  @attribute('string') title;\n}\n\nconst movie = new Movie({title: 'Inception'});\n\nmovie.title // => 'Inception'\n```\n```\n// TS\n\nimport {Component, attribute} from '@layr/component';\n\nclass Movie extends Component {\n  @attribute('string') title!: string;\n}\n\nconst movie = new Movie({title: 'Inception'});\n\nmovie.title // => 'Inception'\n```\n\n##### `create([object], [options])` <badge type=\"secondary\">class method</badge> <badge type=\"outline\">possibly async</badge> {#create-class-method}\n\nCreates an instance of a component class.\n\n**Parameters:**\n\n* `object`: An optional object specifying the value of the component attributes.\n* `options`:\n  * `isNew`: Whether the instance should be marked as new or not (default: `true`).\n  * `source`: A number specifying the [source](https://layrjs.com/docs/v1/reference/attribute#value-source-type) of the created instance (default: `0`).\n  * `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v1/reference/attribute-selector) specifying the attributes to be set (default: `true`, which means that all the attributes will be set).\n  * `attributeFilter`: A (possibly async) function used to filter the attributes to be set. The function is invoked for each attribute with an [`Attribute`](https://layrjs.com/docs/v1/reference/attribute) instance as first argument.\n  * `initialize`: Whether to call the [`initialize`](https://layrjs.com/docs/v1/reference/component#initialize-instance-method) instance method or not (default: `true`).\n\n**Returns:**\n\nAn instance of the component class (possibly a promise if `options.attributeFilter` is an async function or `options.initialize` is `true` and the class has an async [`initialize`](https://layrjs.com/docs/v1/reference/component#initialize-instance-method) instance method).\n\n**Example:**\n\n```\nlet movie = Movie.create({title: 'Inception'});\n\nmovie.title // => 'Inception'\n```\n\n#### Initialization\n\n##### `initialize()` <badge type=\"secondary\">class method</badge> <badge type=\"outline\">possibly async</badge> {#initialize-class-method}\n\nA (possibly async) method that is called automatically when the component class is deserialized. You can override this method in your component subclasses to implement your initialization logic.\n\n##### `initialize()` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">possibly async</badge> {#initialize-instance-method}\n\nA (possibly async) method that is called automatically when the component instance is created or deserialized. You can override this method in your component subclasses to implement your initialization logic.\n\n#### Naming\n\n##### `getComponentName()` <badge type=\"secondary\">class method</badge> {#get-component-name-class-method}\n\nReturns the name of the component, which is usually the name of the corresponding class.\n\n**Returns:**\n\nA string.\n\n**Example:**\n\n```\nMovie.getComponentName(); // => 'Movie'\n```\n\n##### `setComponentName(name)` <badge type=\"secondary\">class method</badge> {#set-component-name-class-method}\n\nSets the name of the component. As the name of a component is usually inferred from the name of its class, this method should not be used so often.\n\n**Parameters:**\n\n* `name`: The name you wish for the component.\n\n**Example:**\n\n```\nMovie.getComponentName(); // => 'Movie'\nMovie.setComponentName('Film');\nMovie.getComponentName(); // => 'Film'\n```\n\n##### `getComponentPath()` <badge type=\"secondary\">class method</badge> {#get-component-path-class-method}\n\nReturns the path of the component starting from its root component.\n\nFor example, if a `Backend` component provides a `Movie` component, this method will return `'Backend.Movie'` when called on the `Movie` component.\n\n**Returns:**\n\nA string.\n\n**Example:**\n\n```\nclass Movie extends Component {}\n\nMovie.getComponentPath(); // => 'Movie'\n\nclass Backend extends Component {\n  @provide() static Movie = Movie;\n}\n\nMovie.getComponentPath(); // => 'Backend.Movie'\n```\n\n#### Typing\n\n##### `getComponentType()` <badge type=\"secondary\">class method</badge> {#get-component-type-class-method}\n\nReturns the type of the component class. A component class type is composed of the component class name prefixed with the string `'typeof '`.\n\nFor example, with a component class named `'Movie'`, this method will return `'typeof Movie'`.\n\n**Returns:**\n\nA string.\n\n**Example:**\n\n```\nMovie.getComponentType(); // => 'typeof Movie'\n```\n\n##### `getComponentType()` <badge type=\"secondary-outline\">instance method</badge> {#get-component-type-instance-method}\n\nReturns the type of the component instance. A component instance type is equivalent to the component class name.\n\nFor example, with a component class named `'Movie'`, this method will return `'Movie'` when called on a `Movie` instance.\n\n**Returns:**\n\nA string.\n\n**Example:**\n\n```\nmovie.getComponentType(); // => 'Movie'\nMovie.prototype.getComponentType(); // => 'Movie'\n```\n\n#### isNew Mark\n\n##### `getIsNewMark()` <badge type=\"secondary-outline\">instance method</badge> {#get-is-new-mark-instance-method}\n\nReturns whether the component instance is marked as new or not.\n\n**Alias:**\n\n`isNew()`\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nlet movie = new Movie();\nmovie.getIsNewMark(); // => true\n\nmovie = Movie.recreate();\nmovie.getIsNewMark(); // => false\n```\n\n##### `setIsNewMark(isNew)` <badge type=\"secondary-outline\">instance method</badge> {#set-is-new-mark-instance-method}\n\nSets whether the component instance is marked as new or not.\n\n**Parameters:**\n\n* `isNew`: A boolean specifying if the component instance should be marked as new or not.\n\n**Example:**\n\n```\nconst movie = new Movie();\nmovie.getIsNewMark(); // => true\nmovie.setIsNewMark(false);\nmovie.getIsNewMark(); // => false\n```\n\n##### `isNew()` <badge type=\"secondary-outline\">instance method</badge> {#is-new-instance-method}\n\nReturns whether the component instance is marked as new or not.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nlet movie = new Movie();\nmovie.isNew(); // => true\n\nmovie = Movie.recreate();\nmovie.isNew(); // => false\n```\n\n##### `markAsNew()` <badge type=\"secondary-outline\">instance method</badge> {#mark-as-new-instance-method}\n\nMarks the component instance as new.\n\nThis method is a shortcut for `setIsNewMark(true)`.\n\n##### `markAsNotNew()` <badge type=\"secondary-outline\">instance method</badge> {#mark-as-not-new-instance-method}\n\nMarks the component instance as not new.\n\nThis method is a shortcut for `setIsNewMark(false)`.\n\n#### Observability\n\nSee the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v1/reference/observable#observable-class) class.\n\n#### Embeddability\n\n##### `isEmbedded()` <badge type=\"secondary\">class method</badge> {#is-embedded-class-method}\n\nReturns whether the component is an [`EmbeddedComponent`](https://layrjs.com/docs/v1/reference/embedded-component).\n\n**Returns:**\n\nA boolean.\n\n#### Properties\n\n##### `getProperty(name)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#get-property-dual-method}\n\nGets a property of the component.\n\n**Parameters:**\n\n* `name`: The name of the property to get.\n\n**Returns:**\n\nAn instance of a [`Property`](https://layrjs.com/docs/v1/reference/property) (or a subclass of [`Property`](https://layrjs.com/docs/v1/reference/property) such as [`Attribute`](https://layrjs.com/docs/v1/reference/attribute), [`Method`](https://layrjs.com/docs/v1/reference/method), etc.).\n\n**Example:**\n\n```\nmovie.getProperty('title'); // => 'title' attribute property\nmovie.getProperty('play'); // => 'play()' method property\n```\n\n##### `hasProperty(name)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#has-property-dual-method}\n\nReturns whether the component has the specified property.\n\n**Parameters:**\n\n* `name`: The name of the property to check.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nmovie.hasProperty('title'); // => true\nmovie.hasProperty('play'); // => true\nmovie.hasProperty('name'); // => false\n```\n\n##### `setProperty(name, PropertyClass, [propertyOptions])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#set-property-dual-method}\n\nDefines a property in the component. Typically, instead of using this method, you would rather use a decorator such as [`@attribute()`](https://layrjs.com/docs/v1/reference/component#attribute-decorator) or [`@method()`](https://layrjs.com/docs/v1/reference/component#method-decorator).\n\n**Parameters:**\n\n* `name`: The name of the property to define.\n* `PropertyClass`: The class of the property (e.g., [`Attribute`](https://layrjs.com/docs/v1/reference/attribute), [`Method`](https://layrjs.com/docs/v1/reference/method)) to use.\n* `propertyOptions`: The options to create the `PropertyClass`.\n\n**Returns:**\n\nThe property that was created.\n\n**Example:**\n\n```\nMovie.prototype.setProperty('title', Attribute, {valueType: 'string'});\n```\n\n##### `deleteProperty(name)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#delete-property-dual-method}\n\nRemoves a property from the component. If the specified property doesn't exist, nothing happens.\n\n**Parameters:**\n\n* `name`: The name of the property to remove.\n\n**Returns:**\n\nA boolean.\n\n##### `getProperties([options])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#get-properties-dual-method}\n\nReturns an iterator providing the properties of the component.\n\n**Parameters:**\n\n* `options`:\n  * `filter`: A function used to filter the properties to be returned. The function is invoked for each property with a [`Property`](https://layrjs.com/docs/v1/reference/property) instance as first argument.\n  * `attributesOnly`: A boolean specifying whether only attribute properties should be returned (default: `false`).\n  * `setAttributesOnly`: A boolean specifying whether only set attributes should be returned (default: `false`).\n  * `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v1/reference/attribute-selector) specifying the attributes to be returned (default: `true`, which means that all the attributes should be returned).\n  * `methodsOnly`: A boolean specifying whether only method properties should be returned (default: `false`).\n\n**Returns:**\n\nA [`Property`](https://layrjs.com/docs/v1/reference/property) instance iterator.\n\n**Example:**\n\n```\nfor (const property of movie.getProperties()) {\n  console.log(property.getName());\n}\n\n// Should output:\n// title\n// play\n```\n\n##### `getPropertyNames()` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#get-property-names-dual-method}\n\nReturns the name of all the properties of the component.\n\n**Returns:**\n\nAn array of the property names.\n\n**Example:**\n\n```\nmovie.getPropertyNames(); // => ['title', 'play']\n```\n\n#### Attribute Properties\n\n##### `getAttribute(name)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#get-attribute-dual-method}\n\nGets an attribute of the component.\n\n**Parameters:**\n\n* `name`: The name of the attribute to get.\n\n**Returns:**\n\nAn instance of [`Attribute`](https://layrjs.com/docs/v1/reference/attribute).\n\n**Example:**\n\n```\nmovie.getAttribute('title'); // => 'title' attribute property\nmovie.getAttribute('play'); // => Error ('play' is a method)\n```\n\n##### `hasAttribute(name)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#has-attribute-dual-method}\n\nReturns whether the component has the specified attribute.\n\n**Parameters:**\n\n* `name`: The name of the attribute to check.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nmovie.hasAttribute('title'); // => true\nmovie.hasAttribute('name'); // => false\nmovie.hasAttribute('play'); // => Error ('play' is a method)\n```\n\n##### `setAttribute(name, [attributeOptions])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#set-attribute-dual-method}\n\nDefines an attribute in the component. Typically, instead of using this method, you would rather use the [`@attribute()`](https://layrjs.com/docs/v1/reference/component#attribute-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the attribute to define.\n* `attributeOptions`: The options to create the [`Attribute`](https://layrjs.com/docs/v1/reference/attribute#constructor).\n\n**Returns:**\n\nThe [`Attribute`](https://layrjs.com/docs/v1/reference/attribute) that was created.\n\n**Example:**\n\n```\nMovie.prototype.setAttribute('title', {valueType: 'string'});\n```\n\n##### `getAttributes([options])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#get-attributes-dual-method}\n\nReturns an iterator providing the attributes of the component.\n\n**Parameters:**\n\n* `options`:\n  * `filter`: A function used to filter the attributes to be returned. The function is invoked for each attribute with an [`Attribute`](https://layrjs.com/docs/v1/reference/attribute) instance as first argument.\n  * `setAttributesOnly`: A boolean specifying whether only set attributes should be returned (default: `false`).\n  * `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v1/reference/attribute-selector) specifying the attributes to be returned (default: `true`, which means that all the attributes should be returned).\n\n**Returns:**\n\nAn [`Attribute`](https://layrjs.com/docs/v1/reference/attribute) instance iterator.\n\n**Example:**\n\n```\nfor (const attr of movie.getAttributes()) {\n  console.log(attr.getName());\n}\n\n// Should output:\n// title\n```\n\n##### `getIdentifierAttribute(name)` <badge type=\"secondary-outline\">instance method</badge> {#get-identifier-attribute-instance-method}\n\nGets an identifier attribute of the component.\n\n**Parameters:**\n\n* `name`: The name of the identifier attribute to get.\n\n**Returns:**\n\nAn instance of [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/primary-identifier-attribute) or [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/secondary-identifier-attribute).\n\n**Example:**\n\n```\nmovie.getIdentifierAttribute('id'); // => 'id' primary identifier attribute\nmovie.getIdentifierAttribute('slug'); // => 'slug' secondary identifier attribute\n```\n\n##### `hasIdentifierAttribute(name)` <badge type=\"secondary-outline\">instance method</badge> {#has-identifier-attribute-instance-method}\n\nReturns whether the component has the specified identifier attribute.\n\n**Parameters:**\n\n* `name`: The name of the identifier attribute to check.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nmovie.hasIdentifierAttribute('id'); // => true\nmovie.hasIdentifierAttribute('slug'); // => true\nmovie.hasIdentifierAttribute('name'); // => false (the property 'name' doesn't exist)\nmovie.hasIdentifierAttribute('title'); // => Error ('title' is not an identifier attribute)\n```\n\n##### `getPrimaryIdentifierAttribute()` <badge type=\"secondary-outline\">instance method</badge> {#get-primary-identifier-attribute-instance-method}\n\nGets the primary identifier attribute of the component.\n\n**Returns:**\n\nAn instance of [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/primary-identifier-attribute).\n\n**Example:**\n\n```\nmovie.getPrimaryIdentifierAttribute(); // => 'id' primary identifier attribute\n```\n\n##### `hasPrimaryIdentifierAttribute()` <badge type=\"secondary-outline\">instance method</badge> {#has-primary-identifier-attribute-instance-method}\n\nReturns whether the component as a primary identifier attribute.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nmovie.hasPrimaryIdentifierAttribute(); // => true\n```\n\n##### `setPrimaryIdentifierAttribute(name, [attributeOptions])` <badge type=\"secondary-outline\">instance method</badge> {#set-primary-identifier-attribute-instance-method}\n\nDefines the primary identifier attribute of the component. Typically, instead of using this method, you would rather use the [`@primaryIdentifier()`](https://layrjs.com/docs/v1/reference/component#primary-identifier-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the primary identifier attribute to define.\n* `attributeOptions`: The options to create the [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/primary-identifier-attribute).\n\n**Returns:**\n\nThe [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/primary-identifier-attribute) that was created.\n\n**Example:**\n\n```\nUser.prototype.setPrimaryIdentifierAttribute('id', {\n  valueType: 'number',\n  default() {\n    return Math.random();\n  }\n});\n```\n\n##### `getSecondaryIdentifierAttribute(name)` <badge type=\"secondary-outline\">instance method</badge> {#get-secondary-identifier-attribute-instance-method}\n\nGets a secondary identifier attribute of the component.\n\n**Parameters:**\n\n* `name`: The name of the secondary identifier attribute to get.\n\n**Returns:**\n\nA [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/secondary-identifier-attribute) instance.\n\n**Example:**\n\n```\nmovie.getSecondaryIdentifierAttribute('slug'); // => 'slug' secondary identifier attribute\nmovie.getSecondaryIdentifierAttribute('id'); // => Error ('id' is not a secondary identifier attribute)\n```\n\n##### `hasSecondaryIdentifierAttribute(name)` <badge type=\"secondary-outline\">instance method</badge> {#has-secondary-identifier-attribute-instance-method}\n\nReturns whether the component has the specified secondary identifier attribute.\n\n**Parameters:**\n\n* `name`: The name of the secondary identifier attribute to check.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nmovie.hasSecondaryIdentifierAttribute('slug'); // => true\nmovie.hasSecondaryIdentifierAttribute('name'); // => false (the property 'name' doesn't exist)\nmovie.hasSecondaryIdentifierAttribute('id'); // => Error ('id' is not a secondary identifier attribute)\n```\n\n##### `setSecondaryIdentifierAttribute(name, [attributeOptions])` <badge type=\"secondary-outline\">instance method</badge> {#set-secondary-identifier-attribute-instance-method}\n\nDefines a secondary identifier attribute in the component. Typically, instead of using this method, you would rather use the [`@secondaryIdentifier()`](https://layrjs.com/docs/v1/reference/component#secondary-identifier-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the secondary identifier attribute to define.\n* `attributeOptions`: The options to create the [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/secondary-identifier-attribute).\n\n**Returns:**\n\nThe [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/secondary-identifier-attribute) that was created.\n\n**Example:**\n\n```\nUser.prototype.setSecondaryIdentifierAttribute('slug', {valueType: 'string'});\n```\n\n##### `getIdentifierAttributes([options])` <badge type=\"secondary-outline\">instance method</badge> {#get-identifier-attributes-instance-method}\n\nReturns an iterator providing the identifier attributes of the component.\n\n**Parameters:**\n\n* `options`:\n  * `filter`: A function used to filter the identifier attributes to be returned. The function is invoked for each identifier attribute with an `IdentifierAttribute` instance as first argument.\n  * `setAttributesOnly`: A boolean specifying whether only set identifier attributes should be returned (default: `false`).\n  * `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v1/reference/attribute-selector) specifying the identifier attributes to be returned (default: `true`, which means that all identifier attributes should be returned).\n\n**Returns:**\n\nAn iterator of [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/primary-identifier-attribute) or [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/secondary-identifier-attribute).\n\n**Example:**\n\n```\nfor (const attr of movie.getIdentifierAttributes()) {\n  console.log(attr.getName());\n}\n\n// Should output:\n// id\n// slug\n```\n\n##### `getSecondaryIdentifierAttributes([options])` <badge type=\"secondary-outline\">instance method</badge> {#get-secondary-identifier-attributes-instance-method}\n\nReturns an iterator providing the secondary identifier attributes of the component.\n\n**Parameters:**\n\n* `options`:\n  * `filter`: A function used to filter the secondary identifier attributes to be returned. The function is invoked for each identifier attribute with a [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/secondary-identifier-attribute) instance as first argument.\n  * `setAttributesOnly`: A boolean specifying whether only set secondary identifier attributes should be returned (default: `false`).\n  * `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v1/reference/attribute-selector) specifying the secondary identifier attributes to be returned (default: `true`, which means that all secondary identifier attributes should be returned).\n\n**Returns:**\n\nA [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/secondary-identifier-attribute) instance iterator.\n\n**Example:**\n\n```\nfor (const attr of movie.getSecondaryIdentifierAttributes()) {\n  console.log(attr.getName());\n}\n\n// Should output:\n// slug\n```\n\n##### `getIdentifiers()` <badge type=\"secondary-outline\">instance method</badge> {#get-identifiers-instance-method}\n\nReturns an object composed of all the set identifiers of the component. The shape of the returned object is `{[identifierName]: identifierValue}`. Throws an error if the component doesn't have any set identifiers.\n\n**Returns:**\n\nAn object.\n\n**Example:**\n\n```\nmovie.getIdentifiers(); // => {id: 'abc123', slug: 'inception'}\n```\n\n##### `hasIdentifiers()` <badge type=\"secondary-outline\">instance method</badge> {#has-identifiers-instance-method}\n\nReturns whether the component has a set identifier or not.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nmovie.hasIdentifiers(); // => true\n```\n\n##### `generateId()` <badge type=\"secondary\">class method</badge> {#generate-id-class-method}\n\nGenerates a unique identifier using the [cuid](https://github.com/ericelliott/cuid) library.\n\n**Returns:**\n\nThe generated identifier.\n\n**Example:**\n\n```\nMovie.generateId(); // => 'ck41vli1z00013h5xx1esffyn'\n```\n\n#### Identifier Descriptor\n\n##### `getIdentifierDescriptor()` <badge type=\"secondary-outline\">instance method</badge> {#get-identifier-descriptor-instance-method}\n\nReturns the `IdentifierDescriptor` of the component.\n\nAn `IdentifierDescriptor` is a plain object composed of one pair of name/value corresponding to the name and value of the first identifier attribute encountered in a component. Usually it is the primary identifier, but if the latter is not set, it can be a secondary identifier.\n\nIf there is no set identifier in the component, an error is thrown.\n\n**Returns:**\n\nAn object.\n\n**Example:**\n\n```\nmovie.getIdentifierDescriptor(); // => {id: 'abc123'}\n```\n\n##### `hasIdentifierDescriptor()` <badge type=\"secondary-outline\">instance method</badge> {#has-identifier-descriptor-instance-method}\n\nReturns whether the component can provide an `IdentifierDescriptor` (using the [`getIdentifierDescriptor()`](https://layrjs.com/docs/v1/reference/component#get-identifier-descriptor-instance-method) method) or not.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nmovie.hasIdentifierDescriptor(); // => true\n```\n\n#### Identity Mapping\n\n##### `getIdentityMap()` <badge type=\"secondary\">class method</badge> {#get-identity-map-class-method}\n\nGets the [`IdentityMap`](https://layrjs.com/docs/v1/reference/identity-map) of the component.\n\n**Returns:**\n\nAn [`IdentityMap`](https://layrjs.com/docs/v1/reference/identity-map) instance.\n\n##### `attach()` <badge type=\"secondary\">class method</badge> {#attach-class-method}\n\nAttaches the component class to its [`IdentityMap`](https://layrjs.com/docs/v1/reference/identity-map). By default, all component classes are attached, so unless you have detached a component class earlier, you should not have to use this method.\n\n**Returns:**\n\nThe component class.\n\n##### `detach()` <badge type=\"secondary\">class method</badge> {#detach-class-method}\n\nDetaches the component class from its [`IdentityMap`](https://layrjs.com/docs/v1/reference/identity-map).\n\n**Returns:**\n\nThe component class.\n\n##### `isAttached()` <badge type=\"secondary\">class method</badge> {#is-attached-class-method}\n\nReturns whether the component class is attached to its [`IdentityMap`](https://layrjs.com/docs/v1/reference/identity-map).\n\n**Returns:**\n\nA boolean.\n\n##### `isDetached()` <badge type=\"secondary\">class method</badge> {#is-detached-class-method}\n\nReturns whether the component class is detached from its [`IdentityMap`](https://layrjs.com/docs/v1/reference/identity-map).\n\n**Returns:**\n\nA boolean.\n\n##### `attach()` <badge type=\"secondary-outline\">instance method</badge> {#attach-instance-method}\n\nAttaches the component instance to its [`IdentityMap`](https://layrjs.com/docs/v1/reference/identity-map). By default, all component instances are attached, so unless you have detached a component instance earlier, you should not have to use this method.\n\n**Returns:**\n\nThe component instance.\n\n##### `detach()` <badge type=\"secondary-outline\">instance method</badge> {#detach-instance-method}\n\nDetaches the component instance from its [`IdentityMap`](https://layrjs.com/docs/v1/reference/identity-map).\n\n**Returns:**\n\nThe component instance.\n\n##### `isAttached()` <badge type=\"secondary-outline\">instance method</badge> {#is-attached-instance-method}\n\nReturns whether the component instance is attached to its [`IdentityMap`](https://layrjs.com/docs/v1/reference/identity-map).\n\n**Returns:**\n\nA boolean.\n\n##### `isDetached()` <badge type=\"secondary-outline\">instance method</badge> {#is-detached-instance-method}\n\nReturns whether the component instance is detached from its [`IdentityMap`](https://layrjs.com/docs/v1/reference/identity-map).\n\n**Returns:**\n\nA boolean.\n\n#### Validation\n\n##### `validate([attributeSelector])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#validate-dual-method}\n\nValidates the attributes of the component. If an attribute doesn't pass the validation, an error is thrown. The error is a JavaScript `Error` instance with a `failedValidators` custom attribute which contains the result of the [`runValidators()`](https://layrjs.com/docs/v1/reference/component#run-validators-dual-method) method.\n\n**Parameters:**\n\n* `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v1/reference/attribute-selector) specifying the attributes to be validated (default: `true`, which means that all the attributes will be validated).\n\n**Example:**\n\n```\n// JS\n\nimport {Component, attribute, validators} from '@layr/component';\n\nconst {notEmpty} = validators;\n\nclass Movie extends Component {\n  @attribute('string', {validators: [notEmpty()]}) title;\n}\n\nconst movie = new Movie({title: 'Inception'});\n\nmovie.title; // => 'Inception'\nmovie.validate(); // All good!\nmovie.title = '';\nmovie.validate(); // Error {failedValidators: [{validator: ..., path: 'title'}]}\n```\n```\n// TS\n\nimport {Component, attribute, validators} from '@layr/component';\n\nconst {notEmpty} = validators;\n\nclass Movie extends Component {\n  @attribute('string', {validators: [notEmpty()]}) title!: string;\n}\n\nconst movie = new Movie({title: 'Inception'});\n\nmovie.title; // => 'Inception'\nmovie.validate(); // All good!\nmovie.title = '';\nmovie.validate(); // Error {failedValidators: [{validator: ..., path: 'title'}]}\n```\n\n##### `isValid([attributeSelector])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#is-valid-dual-method}\n\nReturns whether the attributes of the component are valid.\n\n**Parameters:**\n\n* `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v1/reference/attribute-selector) specifying the attributes to be checked (default: `true`, which means that all the attributes will be checked).\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\n// See the `movie` definition in the `validate()` example\n\nmovie.title; // => 'Inception'\nmovie.isValid(); // => true\nmovie.title = '';\nmovie.isValid(); // => false\n```\n\n##### `runValidators([attributeSelector])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#run-validators-dual-method}\n\nRuns the validators for all the set attributes of the component.\n\n**Parameters:**\n\n* `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v1/reference/attribute-selector) specifying the attributes to be validated (default: `true`, which means that all the attributes will be validated).\n\n**Returns:**\n\nAn array containing the validators that have failed. Each item is a plain object composed of a `validator` (a [`Validator`](https://layrjs.com/docs/v1/reference/validator) instance) and a `path` (a string representing the path of the attribute containing the validator that has failed).\n\n**Example:**\n\n```\n// See the `movie` definition in the `validate()` example\n\nmovie.title; // => 'Inception'\nmovie.runValidators(); // => []\nmovie.title = '';\nmovie.runValidators(); // => [{validator: ..., path: 'title'}]\n```\n\n#### Method Properties\n\n##### `getMethod(name)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#get-method-dual-method}\n\nGets a method of the component.\n\n**Parameters:**\n\n* `name`: The name of the method to get.\n\n**Returns:**\n\nA [`Method`](https://layrjs.com/docs/v1/reference/method) instance.\n\n**Example:**\n\n```\nmovie.getMethod('play'); // => 'play' method property\nmovie.getMethod('title'); // => Error ('title' is an attribute property)\n```\n\n##### `hasMethod(name)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#has-method-dual-method}\n\nReturns whether the component has the specified method.\n\n**Parameters:**\n\n* `name`: The name of the method to check.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nmovie.hasMethod('play'); // => true\nmovie.hasMethod('destroy'); // => false\nmovie.hasMethod('title'); // => Error ('title' is an attribute property)\n```\n\n##### `setMethod(name, [methodOptions])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#set-method-dual-method}\n\nDefines a method in the component. Typically, instead of using this method, you would rather use the [`@method()`](https://layrjs.com/docs/v1/reference/component#method-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the method to define.\n* `methodOptions`: The options to create the [`Method`](https://layrjs.com/docs/v1/reference/method#constructor).\n\n**Returns:**\n\nThe [`Method`](https://layrjs.com/docs/v1/reference/method) that was created.\n\n**Example:**\n\n```\nMovie.prototype.setMethod('play');\n```\n\n##### `getMethods([options])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#get-methods-dual-method}\n\nReturns an iterator providing the methods of the component.\n\n**Parameters:**\n\n* `options`:\n  * `filter`: A function used to filter the methods to be returned. The function is invoked for each method with a [`Method`](https://layrjs.com/docs/v1/reference/method) instance as first argument.\n\n**Returns:**\n\nA [`Method`](https://layrjs.com/docs/v1/reference/method) instance iterator.\n\n**Example:**\n\n```\nfor (const meth of movie.getMethods()) {\n  console.log(meth.getName());\n}\n\n// Should output:\n// play\n```\n\n#### Dependency Management\n\n##### `getComponent(name)` <badge type=\"secondary\">class method</badge> {#get-component-class-method}\n\nGets a component class that is provided or consumed by the current component. An error is thrown if there is no component matching the specified name. If the specified name is the name of the current component, the latter is returned.\n\n**Parameters:**\n\n* `name`: The name of the component class to get.\n\n**Returns:**\n\nA component class.\n\n**Example:**\n\n```\nclass Backend extends Component {\n  @provide() static Movie = Movie;\n}\n\nBackend.getComponent('Movie'); // => Movie\nBackend.getComponent('Backend'); // => Backend\n```\n\n##### `hasComponent(name)` <badge type=\"secondary\">class method</badge> {#has-component-class-method}\n\nReturns whether the current component provides or consumes another component.\n\n**Parameters:**\n\n* `name`: The name of the component class to check.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nclass Backend extends Component {\n  @provide() static Movie = Movie;\n}\n\nBackend.hasComponent('Movie'); // => true\nBackend.hasComponent('Backend'); // => true\nBackend.hasComponent('Film'); // => false\n```\n\n##### `getComponentOfType(type)` <badge type=\"secondary\">class method</badge> {#get-component-of-type-class-method}\n\nGets a component class or prototype of the specified type that is provided or consumed by the current component. An error is thrown if there is no component matching the specified type. If the specified type is the type of the current component, the latter is returned.\n\n**Parameters:**\n\n* `type`: The type of the component class or prototype to get.\n\n**Returns:**\n\nA component class or prototype.\n\n**Example:**\n\n```\nclass Backend extends Component {\n  @provide() static Movie = Movie;\n}\n\nBackend.getComponentOfType('typeof Movie'); // => Movie\nBackend.getComponentOfType('Movie'); // => Movie.prototype\nBackend.getComponentOfType('typeof Backend'); // => Backend\nBackend.getComponentOfType('Backend'); // => Backend.prototype\n```\n\n##### `hasComponentOfType(type)` <badge type=\"secondary\">class method</badge> {#has-component-of-type-class-method}\n\nReturns whether the current component provides or consumes a component class or prototype matching the specified type.\n\n**Parameters:**\n\n* `type`: The type of the component class or prototype to check.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nclass Backend extends Component {\n  @provide() static Movie = Movie;\n}\n\nBackend.hasComponentOfType('typeof Movie'); // => true\nBackend.hasComponentOfType('Movie'); // => true\nBackend.hasComponentOfType('typeof Backend'); // => true\nBackend.hasComponentOfType('Backend'); // => true\nBackend.hasComponentOfType('typeof Film'); // => false\nBackend.hasComponentOfType('Film'); // => false\n```\n\n##### `getProvidedComponent(name)` <badge type=\"secondary\">class method</badge> {#get-provided-component-class-method}\n\nGets a component that is provided by the current component. An error is thrown if there is no provided component with the specified name.\n\n**Parameters:**\n\n* `name`: The name of the provided component to get.\n\n**Returns:**\n\nA component class.\n\n**Example:**\n\n```\nclass Backend extends Component {\n  @provide() static Movie = Movie;\n}\n\nBackend.getProvidedComponent('Movie'); // => Movie\n```\n\n##### `provideComponent(component)` <badge type=\"secondary\">class method</badge> {#provide-component-class-method}\n\nSpecifies that the current component is providing another component so it can be easily accessed from the current component or from any component that is \"consuming\" it using the [`consumeComponent()`](https://layrjs.com/docs/v1/reference/component#consume-component-class-method) method or the [`@consume()`](https://layrjs.com/docs/v1/reference/component#consume-decorator) decorator.\n\nThe provided component can later be accessed using a component accessor that was automatically set on the component provider.\n\nTypically, instead of using this method, you would rather use the [`@provide()`]((https://layrjs.com/docs/v1/reference/component#provide-decorator)) decorator.\n\n**Parameters:**\n\n* `component`: The component class to provide.\n\n**Example:**\n\n```\nclass Backend extends Component {}\nclass Movie extends Component {}\nBackend.provideComponent(Movie);\n\nBackend.Movie; // => `Movie` class\n```\n\n##### `getProvidedComponents([options])` <badge type=\"secondary\">class method</badge> {#get-provided-components-class-method}\n\nReturns an iterator allowing to iterate over the components provided by the current component.\n\n**Parameters:**\n\n* `options`:\n  * `filter`: A function used to filter the provided components to be returned. The function is invoked for each provided component with the provided component as first argument.\n  * `deep`: A boolean specifying whether the method should get the provided components recursively (i.e., get the provided components of the provided components). Default: `false`.\n\n**Returns:**\n\nA component iterator.\n\n##### `getComponentProvider()` <badge type=\"secondary\">class method</badge> {#get-component-provider-class-method}\n\nReturns the provider of the component. If there is no component provider, returns the current component.\n\n**Returns:**\n\nA component provider.\n\n**Example:**\n\n```\nclass Backend extends Component {}\nclass Movie extends Component {}\nBackend.provideComponent(Movie);\n\nMovie.getComponentProvider(); // => `Backend` class\nBackend.getComponentProvider(); // => `Backend` class\n```\n\n##### `getConsumedComponent(name)` <badge type=\"secondary\">class method</badge> {#get-consumed-component-class-method}\n\nGets a component that is consumed by the current component. An error is thrown if there is no consumed component with the specified name. Typically, instead of using this method, you would rather use the component accessor that has been automatically set for you.\n\n**Parameters:**\n\n* `name`: The name of the consumed component to get.\n\n**Returns:**\n\nA component class.\n\n**Example:**\n\n```\n// JS\n\nclass Movie extends Component {\n  @consume() static Actor;\n}\n\nclass Actor extends Component {}\n\nclass Backend extends Component {\n  @provide() static Movie = Movie;\n  @provide() static Actor = Actor;\n}\n\nMovie.getConsumedComponent('Actor'); // => Actor\n\n// Typically, you would rather use the component accessor:\nMovie.Actor; // => Actor\n```\n```\n// TS\n\nclass Movie extends Component {\n  @consume() static Actor: typeof Actor;\n}\n\nclass Actor extends Component {}\n\nclass Backend extends Component {\n  @provide() static Movie = Movie;\n  @provide() static Actor = Actor;\n}\n\nMovie.getConsumedComponent('Actor'); // => Actor\n\n// Typically, you would rather use the component accessor:\nMovie.Actor; // => Actor\n```\n\n##### `consumeComponent(name)` <badge type=\"secondary\">class method</badge> {#consume-component-class-method}\n\nSpecifies that the current component is consuming another component so it can be easily accessed using a component accessor.\n\nTypically, instead of using this method, you would rather use the [`@consume()`]((https://layrjs.com/docs/v1/reference/component#consume-decorator)) decorator.\n\n**Parameters:**\n\n* `name`: The name of the component to consume.\n\n**Example:**\n\n```\nclass Backend extends Component {}\nclass Movie extends Component {}\nBackend.provideComponent(Movie);\nMovie.consumeComponent('Backend');\n\nMovie.Backend; // => `Backend` class\n```\n\n##### `getConsumedComponents([options])` <badge type=\"secondary\">class method</badge> {#get-consumed-components-class-method}\n\nReturns an iterator allowing to iterate over the components consumed by the current component.\n\n**Parameters:**\n\n* `options`:\n  * `filter`: A function used to filter the consumed components to be returned. The function is invoked for each consumed component with the consumed component as first argument.\n\n**Returns:**\n\nA component iterator.\n\n#### Cloning\n\n##### `clone()` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">possibly async</badge> {#clone-instance-method}\n\nClones the component instance. All primitive attributes are copied, and embedded components are cloned recursively. Currently, identifiable components (i.e., components having an identifier attribute) cannot be cloned, but this might change in the future.\n\n**Returns:**\n\nA clone of the component.\n\n**Example:**\n\n```\nmovie.title = 'Inception';\n\nconst movieClone = movie.clone();\nmovieClone.title = 'Inception 2';\n\nmovieClone.title; // => 'Inception 2'\nmovie.title; // => 'Inception'\n```\n\n##### `clone(value)` <badge type=\"tertiary-outline\">function</badge> <badge type=\"outline\">possibly async</badge> {#clone-function}\n\nDeeply clones any type of values including objects, arrays, and component instances (using Component's [`clone()`](https://layrjs.com/docs/v1/reference/component#clone-instance-method) instance method).\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA clone of the specified value.\n\n**Example:**\n\n```\nimport {clone} from '@layr/component';\n\nconst data = {\n  token: 'xyz123',\n  timestamp: 1596600889609,\n  movie: new Movie({title: 'Inception'})\n};\n\nconst dataClone = clone(data);\ndataClone.token; // => 'xyz123';\ndataClone.timestamp; // => 1596600889609\ndataClone.movie; // => A clone of data.movie\n```\n\n#### Forking\n\n##### `fork()` <badge type=\"secondary\">class method</badge> {#fork-class-method}\n\nCreates a fork of the component class.\n\n**Returns:**\n\nThe component class fork.\n\n**Example:**\n\n```\nclass Movie extends Component {}\n\nMovie.fork(); // => A fork of the `Movie` class\n```\n\n##### `fork()` <badge type=\"secondary-outline\">instance method</badge> {#fork-instance-method}\n\nCreates a fork of the component instance. Note that the constructor of the resulting component will be a fork of the component class.\n\n**Returns:**\n\nThe component instance fork.\n\n**Example:**\n\n```\nclass Movie extends Component {}\nconst movie = new Movie();\n\nmovie.fork(); // => A fork of `movie`\nmovie.fork().constructor.isForkOf(Movie); // => true\n```\n\n##### `isForkOf()` <badge type=\"secondary\">class method</badge> {#is-fork-of-class-method}\n\nReturns whether the component class is a fork of another component class.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nclass Movie extends Component {}\nconst MovieFork = Movie.fork();\n\nMovieFork.isForkOf(Movie); // => true\nMovie.isForkOf(MovieFork); // => false\n```\n\n##### `isForkOf()` <badge type=\"secondary-outline\">instance method</badge> {#is-fork-of-instance-method}\n\nReturns whether the component instance is a fork of another component instance.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nclass Movie extends Component {}\nconst movie = new Movie();\nconst movieFork = movie.fork();\n\nmovieFork.isForkOf(movie); // => true\nmovie.isForkOf(movieFork); // => false\n```\n\n##### `getGhost()` <badge type=\"secondary\">class method</badge> {#get-ghost-class-method}\n\nGets the ghost of the component class. A ghost is like a fork, but it is unique. The first time you call this method, a fork is created, and then, all the successive calls return the same fork.\n\n**Returns:**\n\nThe ghost of the component class.\n\n**Example:**\n\n```\nclass Movie extends Component {}\n\nMovie.getGhost() // => A fork of the `Movie` class\nMovie.getGhost() // => The same fork of the `Movie` class\n```\n\n##### `getGhost()` <badge type=\"secondary-outline\">instance method</badge> {#get-ghost-instance-method}\n\nGets the ghost of the component instance. A ghost is like a fork, but it is unique. The first time you call this method, a fork is created, and then, all the successive calls return the same fork. Only identifiable components (i.e., components having an identifier attribute) can be \"ghosted\".\n\n**Returns:**\n\nThe ghost of the component instance.\n\n**Example:**\n\n```\n// JS\n\nclass Movie extends Component {\n  @primaryIdentifier() id;\n}\n\nconst movie = new Movie();\n\nmovie.getGhost() // => A fork of `movie`\nmovie.getGhost() // => The same fork of `movie`\n```\n```\n// TS\n\nclass Movie extends Component {\n  @primaryIdentifier() id!: string;\n}\n\nconst movie = new Movie();\n\nmovie.getGhost() // => A fork of `movie`\nmovie.getGhost() // => The same fork of `movie`\n```\n\n##### `fork(value)` <badge type=\"tertiary-outline\">function</badge> {#fork-function}\n\nFork any type of values including objects, arrays, and components (using Component's `fork()` [class method](https://layrjs.com/docs/v1/reference/component#fork-class-method) and [instance method](https://layrjs.com/docs/v1/reference/component#fork-instance-method)).\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA fork of the specified value.\n\n**Example:**\n\n```\nimport {fork} from '@layr/component';\n\nconst data = {\n  token: 'xyz123',\n  timestamp: 1596600889609,\n  movie: new Movie({title: 'Inception'})\n};\n\nconst dataFork = fork(data);\nObject.getPrototypeOf(dataFork); // => data\ndataFork.token; // => 'xyz123';\ndataFork.timestamp; // => 1596600889609\ndataFork.movie.isForkOf(data.movie); // => true\n```\n\n#### Merging\n\n##### `merge(forkedComponent)` <badge type=\"secondary\">class method</badge> {#merge-class-method}\n\nMerges the attributes of a component class fork into the current component class.\n\n**Parameters:**\n\n* `forkedComponent`: The component class fork to merge.\n\n**Returns:**\n\nThe current component class.\n\n**Example:**\n\n```\nclass Movie extends Component {\n  @attribute('string') static customName = 'Movie';\n}\n\nconst MovieFork = Movie.fork();\nMovieFork.customName = 'Film';\n\nMovie.customName; // => 'Movie'\nMovie.merge(MovieFork);\nMovie.customName; // => 'Film'\n```\n\n##### `merge(forkedComponent)` <badge type=\"secondary-outline\">instance method</badge> {#merge-instance-method}\n\nMerges the attributes of a component instance fork into the current component instance.\n\n**Parameters:**\n\n* `forkedComponent`: The component instance fork to merge.\n\n**Returns:**\n\nThe current component instance.\n\n**Example:**\n\n```\nconst movie = new Movie({title: 'Inception'});\nconst movieFork = movie.fork();\nmovieFork.title = 'Inception 2';\n\nmovie.title; // => 'Inception'\nmovie.merge(movieFork);\nmovie.title; // => 'Inception 2'\n```\n\n##### `merge(value, forkedValue)` <badge type=\"tertiary-outline\">function</badge> {#merge-function}\n\nDeeply merge any type of forks including objects, arrays, and components (using Component's `merge()` [class method](https://layrjs.com/docs/v1/reference/component#merge-class-method) and [instance method](https://layrjs.com/docs/v1/reference/component#merge-instance-method)) into their original values.\n\n**Parameters:**\n\n* `value`: An original value of any type.\n* `forkedValue`: A fork of `value`.\n\n**Returns:**\n\nThe original value.\n\n**Example:**\n\n```\nimport {fork, merge} from '@layr/component';\n\nconst data = {\n  token: 'xyz123',\n  timestamp: 1596600889609,\n  movie: new Movie({title: 'Inception'})\n};\n\nconst dataFork = fork(data);\ndataFork.token = 'xyz456';\ndataFork.movie.title = 'Inception 2';\n\ndata.token; // => 'xyz123'\ndata.movie.title; // => 'Inception'\nmerge(data, dataFork);\ndata.token; // => 'xyz456'\ndata.movie.title; // => 'Inception 2'\n```\n\n#### Serialization\n\n##### `serialize([options])` <badge type=\"secondary\">class method</badge> <badge type=\"outline\">possibly async</badge> {#serialize-class-method}\n\nSerializes the component class to a plain object.\n\n**Parameters:**\n\n* `options`:\n  * `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v1/reference/attribute-selector) specifying the attributes to be serialized (default: `true`, which means that all the attributes will be serialized).\n  * `attributeFilter`: A (possibly async) function used to filter the attributes to be serialized. The function is invoked for each attribute with an [`Attribute`](https://layrjs.com/docs/v1/reference/attribute) instance as first argument.\n  * `target`: A number specifying the [target](https://layrjs.com/docs/v1/reference/attribute#value-source-type) of the serialization (default: `undefined`).\n\n**Returns:**\n\nA plain object representing the serialized component class.\n\n**Example:**\n\n```\nclass Movie extends Component {\n  @attribute('string') static customName = 'Film';\n}\n\nMovie.serialize(); // => {__component: 'typeof Movie', customName: 'Film'}\n```\n\n##### `serialize([options])` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">possibly async</badge> {#serialize-instance-method}\n\nSerializes the component instance to a plain object.\n\n**Parameters:**\n\n* `options`:\n  * `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v1/reference/attribute-selector) specifying the attributes to be serialized (default: `true`, which means that all the attributes will be serialized).\n  * `attributeFilter`: A (possibly async) function used to filter the attributes to be serialized. The function is invoked for each attribute with an [`Attribute`](https://layrjs.com/docs/v1/reference/attribute) instance as first argument.\n  * `target`: A number specifying the [target](https://layrjs.com/docs/v1/reference/attribute#value-source-type) of the serialization (default: `undefined`).\n\n**Returns:**\n\nA plain object representing the serialized component instance.\n\n**Example:**\n\n```\nconst movie = new Movie({title: 'Inception'});\n\nmovie.serialize(); // => {__component: 'Movie', title: 'Inception'}\n```\n\n##### `serialize(value, [options])` <badge type=\"tertiary-outline\">function</badge> <badge type=\"outline\">possibly async</badge> {#serialize-function}\n\nSerializes any type of values including objects, arrays, dates, and components (using Component's `serialize()` [class method](https://layrjs.com/docs/v1/reference/component#serialize-class-method) and [instance method](https://layrjs.com/docs/v1/reference/component#serialize-instance-method)).\n\n**Parameters:**\n\n* `value`: A value of any type.\n* `options`:\n  * `attributeFilter`: A (possibly async) function used to filter the component attributes to be serialized. The function is invoked for each attribute with an [`Attribute`](https://layrjs.com/docs/v1/reference/attribute) instance as first argument.\n  * `target`: The target of the serialization (default: `undefined`).\n\n**Returns:**\n\nThe serialized value.\n\n**Example:**\n\n```\nimport {serialize} from '@layr/component';\n\nconst data = {\n  createdOn: new Date(),\n  updatedOn: undefined,\n  movie: new Movie({title: 'Inception'})\n};\n\nconsole.log(serialize(data));\n\n// Should output something like:\n// {\n//   createdOn: {__date: \"2020-07-18T23:43:33.778Z\"},\n//   updatedOn: {__undefined: true},\n//   movie: {__component: 'Movie', title: 'Inception'}\n// }\n```\n\n#### Deserialization\n\n##### `recreate([object], [options])` <badge type=\"secondary\">class method</badge> <badge type=\"outline\">possibly async</badge> {#recreate-class-method}\n\nRecreates a component instance from the result of its serialization.\n\n**Parameters:**\n\n* `object`: A plain object representing a serialized component.\n* `options`:\n  * `attributeFilter`: A (possibly async) function used to filter the attributes to be deserialized. The function is invoked for each attribute with an [`Attribute`](https://layrjs.com/docs/v1/reference/attribute) instance as first argument.\n  * `source`: A number specifying the [source](https://layrjs.com/docs/v1/reference/attribute#value-source-type) of the serialization (default: `0`).\n\n**Returns:**\n\nA component instance.\n\n**Example:**\n\n```\nconst movie = Movie.recreate({title: 'Inception'});\nmovie.title; // => 'Inception'\n```\n\n##### `deserialize([object], [options])` <badge type=\"secondary\">class method</badge> <badge type=\"outline\">possibly async</badge> {#deserialize-class-method}\n\nDeserializes the component class from the specified plain object. The deserialization operates \"in place\", which means that the current component class attributes are mutated.\n\n**Parameters:**\n\n* `object`: The plain object to deserialize from.\n* `options`:\n  * `attributeFilter`: A (possibly async) function used to filter the attributes to be deserialized. The function is invoked for each attribute with an [`Attribute`](https://layrjs.com/docs/v1/reference/attribute) instance as first argument.\n  * `source`: A number specifying the [source](https://layrjs.com/docs/v1/reference/attribute#value-source-type) of the serialization (default: `0`).\n\n**Returns:**\n\nThe component class.\n\n**Example:**\n\n```\nclass Movie extends Component {\n  @attribute('string') static customName = 'Movie';\n}\n\nMovie.customName; // => 'Movie'\nMovie.deserialize({customName: 'Film'});\nMovie.customName; // => 'Film'\n```\n\n##### `deserialize([object], [options])` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">possibly async</badge> {#deserialize-instance-method}\n\nDeserializes the component instance from the specified plain object. The deserialization operates \"in place\", which means that the current component instance attributes are mutated.\n\n**Parameters:**\n\n* `object`: The plain object to deserialize from.\n* `options`:\n  * `attributeFilter`: A (possibly async) function used to filter the attributes to be deserialized. The function is invoked for each attribute with an [`Attribute`](https://layrjs.com/docs/v1/reference/attribute) instance as first argument.\n  * `source`: A number specifying the [source](https://layrjs.com/docs/v1/reference/attribute#value-source-type) of the serialization (default: `0`).\n\n**Returns:**\n\nThe current component instance.\n\n**Example:**\n\n```\nclass Movie extends Component {\n  @attribute('string') title = '';\n}\n\nconst movie = new Movie();\n\nmovie.title; // => ''\nmovie.deserialize({title: 'Inception'});\nmovie.title; // => 'Inception'\n```\n\n##### `deserialize(value, [options])` <badge type=\"tertiary-outline\">function</badge> <badge type=\"outline\">possibly async</badge> {#deserialize-function}\n\nDeserializes any type of serialized values including objects, arrays, dates, and components (using [`Component.recreate()`](https://layrjs.com/docs/v1/reference/component#recreate-class-method) and [`Component.deserialize()`](https://layrjs.com/docs/v1/reference/component#deserialize-class-method)).\n\n**Parameters:**\n\n* `value`: A serialized value.\n* `options`:\n  * `componentGetter`: A function used to get the component classes from the component types encountered in the serialized value. The function is invoked with a string representing a component type and should return a component class or prototype.\n  * `attributeFilter`: A (possibly async) function used to filter the component attributes to be deserialized. The function is invoked for each attribute with an [`Attribute`](https://layrjs.com/docs/v1/reference/attribute) instance as first argument.\n  * `source`: The source of the serialization (default: `0`).\n\n**Returns:**\n\nThe deserialized value.\n\n**Example:**\n\n```\n// JS\n\nimport {Component, deserialize} from '@layr/component';\n\nclass Movie extends Component {\n  @attribute('string') title;\n}\n\nconst serializedData = {\n  createdOn: {__date: \"2020-07-18T23:43:33.778Z\"},\n  updatedOn: {__undefined: true},\n  movie: {__component: 'Movie', title: 'Inception'}\n};\n\nconst data = deserialize(serializedData, {\n  componentGetter(type) {\n    if (type === 'Movie') {\n      return Movie;\n    }\n  }\n});\n\ndata.createdOn; // => A Date instance\ndata.updatedOn; // => undefined\ndata.movie; // => A Movie instance\n```\n```\n// TS\n\nimport {Component, deserialize} from '@layr/component';\n\nclass Movie extends Component {\n  @attribute('string') title!: string;\n}\n\nconst serializedData = {\n  createdOn: {__date: \"2020-07-18T23:43:33.778Z\"},\n  updatedOn: {__undefined: true},\n  movie: {__component: 'Movie', title: 'Inception'}\n};\n\nconst data = deserialize(serializedData, {\n  componentGetter(type: string) {\n    if (type === 'Movie') {\n      return Movie;\n    }\n  }\n});\n\ndata.createdOn; // => A Date instance\ndata.updatedOn; // => undefined\ndata.movie; // => A Movie instance\n```\n\n#### Decorators\n\n##### `@attribute([valueType], [options])` <badge type=\"tertiary\">decorator</badge> {#attribute-decorator}\n\nDecorates an attribute of a component so it can be type checked at runtime, validated, serialized, observed, etc.\n\n**Parameters:**\n\n* `valueType`: A string specifying the [type of values](https://layrjs.com/docs/v1/reference/value-type#supported-types) that can be stored in the attribute (default: `'any'`).\n* `options`: The options to create the [`Attribute`](https://layrjs.com/docs/v1/reference/attribute#constructor).\n\n**Example:**\n\n```\n// JS\n\nimport {Component, attribute, validators} from '@layr/component';\n\nconst {maxLength} = validators;\n\nclass Movie extends Component {\n  // Optional 'string' class attribute\n  @attribute('string?') static customName;\n\n  // Required 'string' instance attribute\n  @attribute('string') title;\n\n  // Optional 'string' instance attribute with a validator\n  @attribute('string?', {validators: [maxLength(100)]}) summary;\n\n  // Required array of 'Actor' instance attribute with a default value\n  @attribute('Actor[]') actors = [];\n}\n```\n```\n// TS\n\nimport {Component, attribute, validators} from '@layr/component';\n\nconst {maxLength} = validators;\n\nclass Movie extends Component {\n  // Optional 'string' class attribute\n  @attribute('string?') static customName?: string;\n\n  // Required 'string' instance attribute\n  @attribute('string') title!: string;\n\n  // Optional 'string' instance attribute with a validator\n  @attribute('string?', {validators: [maxLength(100)]}) summary?: string;\n\n  // Required array of 'Actor' instance attribute with a default value\n  @attribute('Actor[]') actors: Actor[] = [];\n}\n```\n\n##### `@primaryIdentifier([valueType], [options])` <badge type=\"tertiary\">decorator</badge> {#primary-identifier-decorator}\n\nDecorates an attribute of a component as a [primary identifier attribute](https://layrjs.com/docs/v1/reference/primary-identifier-attribute).\n\n**Parameters:**\n\n* `valueType`: A string specifying the type of values the attribute can store. It can be either `'string'` or `'number'` (default: `'string'`).\n* `options`: The options to create the [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/primary-identifier-attribute).\n\n**Example:**\n\n```\n// JS\n\nimport {Component, primaryIdentifier} from '@layr/component';\n\nclass Movie extends Component {\n  // Auto-generated 'string' primary identifier attribute\n  @primaryIdentifier('string') id;\n}\n\nclass Film extends Component {\n  // Custom 'number' primary identifier attribute\n  @primaryIdentifier('number', {default() { return Math.random(); }}) id;\n}\n```\n```\n// TS\n\nimport {Component, primaryIdentifier} from '@layr/component';\n\nclass Movie extends Component {\n  // Auto-generated 'string' primary identifier attribute\n  @primaryIdentifier('string') id!: string;\n}\n\nclass Film extends Component {\n  // Custom 'number' primary identifier attribute\n  @primaryIdentifier('number', {default() { return Math.random(); }}) id!: number;\n}\n```\n\n##### `@secondaryIdentifier([valueType], [options])` <badge type=\"tertiary\">decorator</badge> {#secondary-identifier-decorator}\n\nDecorates an attribute of a component as a [secondary identifier attribute](https://layrjs.com/docs/v1/reference/secondary-identifier-attribute).\n\n**Parameters:**\n\n* `valueType`: A string specifying the type of values the attribute can store. It can be either `'string'` or `'number'` (default: `'string'`).\n* `options`: The options to create the [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/secondary-identifier-attribute).\n\n**Example:**\n\n```\n// JS\n\nimport {Component, secondaryIdentifier} from '@layr/component';\n\nclass Movie extends Component {\n  // 'string' secondary identifier attribute\n  @secondaryIdentifier('string') slug;\n\n  // 'number' secondary identifier attribute\n  @secondaryIdentifier('number') reference;\n}\n```\n```\n// TS\n\nimport {Component, secondaryIdentifier} from '@layr/component';\n\nclass Movie extends Component {\n  // 'string' secondary identifier attribute\n  @secondaryIdentifier('string') slug!: string;\n\n  // 'number' secondary identifier attribute\n  @secondaryIdentifier('number') reference!: number;\n}\n```\n\n##### `@method([options])` <badge type=\"tertiary\">decorator</badge> {#method-decorator}\n\nDecorates a method of a component so it can be exposed and called remotely.\n\n**Parameters:**\n\n* `options`: The options to create the [`Method`](https://layrjs.com/docs/v1/reference/method#constructor).\n\n**Example:**\n\n```\nimport {Component, method} from '@layr/component';\n\nclass Movie extends Component {\n  // Class method\n  @method() static getConfig() {\n    // ...\n  }\n\n  // Instance method\n  @method() play() {\n    // ...\n  }\n}\n```\n\n##### `@expose(exposure)` <badge type=\"tertiary\">decorator</badge> {#expose-decorator}\n\nExposes some attributes or methods of a component so they can be consumed remotely.\n\nThis decorator is usually placed before a component attribute or method, but it can also be placed before a component class. When placed before a component class, you can expose several attributes or methods at once, and even better, you can expose attributes or methods that are defined in a parent class.\n\n**Parameters:**\n\n* `exposure`: An object specifying which operations should be exposed. When the decorator is placed before a component attribute or method, the object is of type [`PropertyExposure`](https://layrjs.com/docs/v1/reference/property#property-exposure-type). When the decorator is placed before a component class, the shape of the object is `{[propertyName]: PropertyExposure, prototype: {[propertyName]: PropertyExposure}}`.\n\n**Example:**\n\n```\n// JS\n\nimport {Component, expose, attribute, method} from '@layr/component';\n\nclass Movie extends Component {\n  // Class attribute exposing the 'get' operation only\n  @expose({get: true}) @attribute('string?') static customName;\n\n  // Instance attribute exposing the 'get' and 'set' operations\n  @expose({get: true, set: true}) @attribute('string') title;\n\n  // Class method exposure\n  @expose({call: true}) @method() static getConfig() {\n    // ...\n  }\n\n  // Instance method exposure\n  @expose({call: true}) @method() play() {\n    // ...\n  }\n}\n\n// Exposing some class and instance methods that are defined in a parent class\n@expose({find: {call: true}, prototype: {load: {call: true}}})\nclass Actor extends Storable(Component) {\n  // ...\n}\n```\n```\n// TS\n\nimport {Component, expose, attribute, method} from '@layr/component';\n\nclass Movie extends Component {\n  // Class attribute exposing the 'get' operation only\n  @expose({get: true}) @attribute('string?') static customName?: string;\n\n  // Instance attribute exposing the 'get' and 'set' operations\n  @expose({get: true, set: true}) @attribute('string') title!: string;\n\n  // Class method exposure\n  @expose({call: true}) @method() static getConfig() {\n    // ...\n  }\n\n  // Instance method exposure\n  @expose({call: true}) @method() play() {\n    // ...\n  }\n}\n\n// Exposing some class and instance methods that are defined in a parent class\n@expose({find: {call: true}, prototype: {load: {call: true}}})\nclass Actor extends Storable(Component) {\n  // ...\n}\n```\n\n##### `@provide()` <badge type=\"tertiary\">decorator</badge> {#provide-decorator}\n\nProvides a component so it can be easily accessed from the current component or from any component that is \"consuming\" it using the [`@consume()`](https://layrjs.com/docs/v1/reference/component#consume-decorator) decorator.\n\n**Example:**\n\n```\n// JS\n\nimport {Component, provide, consume} from '@layr/component';\n\nclass Movie extends Component {\n  @consume() static Actor;\n}\n\nclass Actor extends Component {}\n\nclass Backend extends Component {\n  @provide() static Movie = Movie;\n  @provide() static Actor = Actor;\n}\n\n// Since `Actor` is provided by `Backend`, it can be accessed from `Movie`\nMovie.Actor; // => Actor\n```\n```\n// TS\n\nimport {Component, provide, consume} from '@layr/component';\n\nclass Movie extends Component {\n  @consume() static Actor: typeof Actor;\n}\n\nclass Actor extends Component {}\n\nclass Backend extends Component {\n  @provide() static Movie = Movie;\n  @provide() static Actor = Actor;\n}\n\n// Since `Actor` is provided by `Backend`, it can be accessed from `Movie`\nMovie.Actor; // => Actor\n```\n\n##### `@consume()` <badge type=\"tertiary\">decorator</badge> {#consume-decorator}\n\nConsumes a component provided by the provider (or recursively, any provider's provider) of the current component so it can be easily accessed using a component accessor.\n\n**Example:**\n\nSee [`@provide()`'s example](https://layrjs.com/docs/v1/reference/component#provide-decorator).\n#### Utilities\n\n##### `isComponentClass(value)` <badge type=\"tertiary-outline\">function</badge> {#is-component-class-function}\n\nReturns whether the specified value is a component class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isComponentInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-component-instance-function}\n\nReturns whether the specified value is a component instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isComponentClassOrInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-component-class-or-instance-function}\n\nReturns whether the specified value is a component class or instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `assertIsComponentClass(value)` <badge type=\"tertiary-outline\">function</badge> {#assert-is-component-class-function}\n\nThrows an error if the specified value is not a component class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n##### `assertIsComponentInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#assert-is-component-instance-function}\n\nThrows an error if the specified value is not a component instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n##### `assertIsComponentClassOrInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#assert-is-component-class-or-instance-function}\n\nThrows an error if the specified value is not a component class or instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n##### `ensureComponentClass(component)` <badge type=\"tertiary-outline\">function</badge> {#ensure-component-class-function}\n\nEnsures that the specified component is a class. If you specify a component instance (or prototype), the class of the component is returned. If you specify a component class, it is returned as is.\n\n**Parameters:**\n\n* `component`: A component class or instance.\n\n**Returns:**\n\nA component class.\n\n**Example:**\n\n```\nensureComponentClass(movie) => Movie\nensureComponentClass(Movie.prototype) => Movie\nensureComponentClass(Movie) => Movie\n```\n\n##### `ensureComponentInstance(component)` <badge type=\"tertiary-outline\">function</badge> {#ensure-component-instance-function}\n\nEnsures that the specified component is an instance (or prototype). If you specify a component class, the component prototype is returned. If you specify a component instance (or prototype), it is returned as is.\n\n**Parameters:**\n\n* `component`: A component class or instance.\n\n**Returns:**\n\nA component instance (or prototype).\n\n**Example:**\n\n```\nensureComponentInstance(Movie) => Movie.prototype\nensureComponentInstance(Movie.prototype) => Movie.prototype\nensureComponentInstance(movie) => movie\n```\n\n##### `isComponentName(name)` <badge type=\"tertiary-outline\">function</badge> {#is-component-name-function}\n\nReturns whether the specified string is a valid component name. The rule is the same as for typical JavaScript class names.\n\n**Parameters:**\n\n* `name`: The string to check.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nisComponentName('Movie') => true\nisComponentName('Movie123') => true\nisComponentName('Awesome_Movie') => true\nisComponentName('123Movie') => false\nisComponentName('Awesome-Movie') => false\nisComponentName('movie') => false\n```\n\n##### `assertIsComponentName(name)` <badge type=\"tertiary-outline\">function</badge> {#assert-is-component-name-function}\n\nThrows an error if the specified string is not a valid component name.\n\n**Parameters:**\n\n* `name`: The string to check.\n\n##### `getComponentNameFromComponentClassType(name)` <badge type=\"tertiary-outline\">function</badge> {#get-component-name-from-component-class-type-function}\n\nTransforms a component class type into a component name.\n\n**Parameters:**\n\n* `name`: A string representing a component class type.\n\n**Returns:**\n\nA component name.\n\n**Example:**\n\n```\ngetComponentNameFromComponentClassType('typeof Movie') => 'Movie'\n```\n\n##### `getComponentNameFromComponentInstanceType(name)` <badge type=\"tertiary-outline\">function</badge> {#get-component-name-from-component-instance-type-function}\n\nTransforms a component instance type into a component name.\n\n**Parameters:**\n\n* `name`: A string representing a component instance type.\n\n**Returns:**\n\nA component name.\n\n**Example:**\n\n```\ngetComponentNameFromComponentInstanceType('Movie') => 'Movie'\n```\n\n##### `isComponentType(name, [options])` <badge type=\"tertiary-outline\">function</badge> {#is-component-type-function}\n\nReturns whether the specified string is a valid component type.\n\n**Parameters:**\n\n* `name`: The string to check.\n* `options`:\n  * `allowClasses`: A boolean specifying whether component class types are allowed (default: `true`).\n  * `allowInstances`: A boolean specifying whether component instance types are allowed (default: `true`).\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nisComponentType('typeof Movie') => true\nisComponentType('Movie') => true\nisComponentType('typeof Awesome-Movie') => false\nisComponentType('movie') => false\nisComponentType('typeof Movie', {allowClasses: false}) => false\nisComponentType('Movie', {allowInstances: false}) => false\n```\n\n##### `assertIsComponentType(name, [options])` <badge type=\"tertiary-outline\">function</badge> {#assert-is-component-type-function}\n\nThrows an error if the specified string is not a valid component type.\n\n**Parameters:**\n\n* `name`: The string to check.\n* `options`:\n  * `allowClasses`: A boolean specifying whether component class types are allowed (default: `true`).\n  * `allowInstances`: A boolean specifying whether component instance types are allowed (default: `true`).\n\n##### `getComponentClassTypeFromComponentName(name)` <badge type=\"tertiary-outline\">function</badge> {#get-component-class-type-from-component-name-function}\n\nTransforms a component name into a component class type.\n\n**Parameters:**\n\n* `name`: A component name.\n\n**Returns:**\n\nA component class type.\n\n**Example:**\n\n```\ngetComponentClassTypeFromComponentName('Movie') => 'typeof Movie'\n```\n\n##### `getComponentInstanceTypeFromComponentName(name)` <badge type=\"tertiary-outline\">function</badge> {#get-component-instance-type-from-component-name-function}\n\nTransforms a component name into a component instance type.\n\n**Parameters:**\n\n* `name`: A component name.\n\n**Returns:**\n\nA component instance type.\n\n**Example:**\n\n```\ngetComponentInstanceTypeFromComponentName('Movie') => 'Movie'\n```\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/component-client-1ISSjJNRL12I33addutRh4.immutable.md",
    "content": "### ComponentClient <badge type=\"primary\">class</badge> {#component-client-class}\n\nA base class allowing to access a root [`Component`](https://layrjs.com/docs/v1/reference/component) that is served by a [`ComponentServer`](https://layrjs.com/docs/v1/reference/component-server).\n\nTypically, instead of using this class, you would use a subclass such as [`ComponentHTTPClient`](https://layrjs.com/docs/v1/reference/component-http-client).\n\n#### Creation\n\n##### `new ComponentClient(componentServer, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates a component client.\n\n**Parameters:**\n\n* `componentServer`: The [`ComponentServer`](https://layrjs.com/docs/v1/reference/component-server) to connect to.\n* `options`:\n  * `version`: A number specifying the expected version of the component server (default: `undefined`). If a version is specified, an error is thrown when a request is sent and the component server has a different version. The thrown error is a JavaScript `Error` instance with a `code` attribute set to `'COMPONENT_CLIENT_VERSION_DOES_NOT_MATCH_COMPONENT_SERVER_VERSION'`.\n  * `mixins`: An array of the component mixins (e.g., [`Storable`](https://layrjs.com/docs/v1/reference/storable)) to use when constructing the components exposed by the component server (default: `[]`).\n\n**Returns:**\n\nA `ComponentClient` instance.\n\n**Example:**\n\n```\n// JS\n\nimport {Component, attribute, expose} from '@layr/component';\nimport {ComponentClient} from '@layr/component-client';\nimport {ComponentServer} from '@layr/component-server';\n\nclass Movie extends Component {\n  @expose({get: true, set: true}) @attribute('string') title;\n}\n\nconst server = new ComponentServer(Movie);\nconst client = new ComponentClient(server);\n\nconst RemoteMovie = client.getComponent();\n```\n```\n// TS\n\nimport {Component, attribute, expose} from '@layr/component';\nimport {ComponentClient} from '@layr/component-client';\nimport {ComponentServer} from '@layr/component-server';\n\nclass Movie extends Component {\n  @expose({get: true, set: true}) @attribute('string') title!: string;\n}\n\nconst server = new ComponentServer(Movie);\nconst client = new ComponentClient(server);\n\nconst RemoteMovie = client.getComponent() as typeof Movie;\n```\n\n#### Getting the Served Component\n\n##### `getComponent()` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">possibly async</badge> {#get-component-instance-method}\n\nGets the component that is served by the component server.\n\n**Returns:**\n\nA [`Component`](https://layrjs.com/docs/v1/reference/component) class.\n\n**Example:**\n\nSee [`constructor`'s example](https://layrjs.com/docs/v1/reference/component-client#constructor)."
  },
  {
    "path": "website/frontend/public/docs/v1/reference/component-express-middleware-6UieWJkEvLdXLYug4xuM16.immutable.md",
    "content": "### component-express-middleware <badge type=\"primary\">module</badge> {#component-express-middleware-module}\n\nAn [Express](https://expressjs.com/) middleware allowing to serve a root [`Component`](https://layrjs.com/docs/v1/reference/component) so it can be accessed by a [`ComponentHTTPClient`](https://layrjs.com/docs/v1/reference/component-http-client).\n\n#### Usage\n\nCall the [`serveComponent()`](https://layrjs.com/docs/v1/reference/component-express-middleware#serve-component-function) function to create a middleware for your Express application.\n\n**Example:**\n\n```\nimport express from 'express';\nimport {Component} from '@layr/component';\nimport {serveComponent} from '@layr/component-express-middleware';\n\nclass Movie extends Component {\n  // ...\n}\n\nconst app = express();\n\napp.use('/api', serveComponent(Movie));\n\napp.listen(3210);\n```\n\n#### Functions\n\n##### `serveComponent(componentOrComponentServer, [options])` <badge type=\"tertiary-outline\">function</badge> {#serve-component-function}\n\nCreates an [Express](https://expressjs.com/) middleware exposing the specified root [`Component`](https://layrjs.com/docs/v1/reference/component) class.\n\n**Parameters:**\n\n* `componentOrComponentServer`: The root [`Component`](https://layrjs.com/docs/v1/reference/component) class to serve. An instance of a [`ComponentServer`](https://layrjs.com/docs/v1/reference/component-server) will be created under the hood. Alternatively, you can pass an existing instance of a [`ComponentServer`](https://layrjs.com/docs/v1/reference/component-server).\n* `options`:\n  * `version`: A number specifying the version of the created [`ComponentServer`](https://layrjs.com/docs/v1/reference/component-server) (default: `undefined`).\n\n**Returns:**\n\nAn Express middleware.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/component-http-client-6g8M9kRcBS4AQwFtAp7t9z.immutable.md",
    "content": "### ComponentHTTPClient <badge type=\"primary\">class</badge> {#component-http-client-class}\n\n*Inherits from [`ComponentClient`](https://layrjs.com/docs/v1/reference/component-client).*\n\nA class allowing to access a root [`Component`](https://layrjs.com/docs/v1/reference/component) that is served by a [`ComponentHTTPServer`](https://layrjs.com/docs/v1/reference/component-http-server), a middleware such as [`component-express-middleware`](https://layrjs.com/docs/v1/reference/component-express-middleware), or any HTTP server exposing a [`ComponentServer`](https://layrjs.com/docs/v1/reference/component-server).\n\n#### Usage\n\nCreate an instance of `ComponentHTTPClient` by specifying the URL of the component server, and use the [`getComponent()`](https://layrjs.com/docs/v1/reference/component-http-client#get-component-instance-method) method to get the served component.\n\nFor example, to access a `Movie` component that is served by a component server, you could do the following:\n\n```\n// JS\n\n// backend.js\n\nimport {Component, attribute, method, expose} from '@layr/component';\nimport {ComponentHTTPServer} from '@layr/component-http-server';\n\nexport class Movie extends Component {\n  @expose({get: true, set: true}) @attribute('string') title;\n\n  @expose({call: true}) @method() async play() {\n    return `Playing `${this.title}`...`;\n  }\n}\n\nconst server = new ComponentHTTPServer(Movie, {port: 3210});\n\nserver.start();\n```\n\n```\n// JS\n\n// frontend.js\n\nimport {ComponentHTTPClient} from '@layr/component-http-client';\n\n(async () => {\n  const client = new ComponentHTTPClient('http://localhost:3210');\n\n  const Movie = await client.getComponent();\n\n  const movie = new Movie({title: 'Inception'});\n\n  await movie.play(); // => 'Playing Inception...'\n})();\n```\n\n```\n// TS\n\n// backend.ts\n\nimport {Component, attribute, method, expose} from '@layr/component';\nimport {ComponentHTTPServer} from '@layr/component-http-server';\n\nexport class Movie extends Component {\n  @expose({get: true, set: true}) @attribute('string') title!: string;\n\n  @expose({call: true}) @method() async play() {\n    return `Playing `${this.title}`...`;\n  }\n}\n\nconst server = new ComponentHTTPServer(Movie, {port: 3210});\n\nserver.start();\n```\n\n```\n// TS\n\n// frontend.ts\n\nimport {ComponentHTTPClient} from '@layr/component-http-client';\n\nimport type {Movie as MovieType} from './backend';\n\n(async () => {\n  const client = new ComponentHTTPClient('http://localhost:3210');\n\n  const Movie = (await client.getComponent()) as typeof MovieType;\n\n  const movie = new Movie({title: 'Inception'});\n\n  await movie.play(); // => 'Playing Inception...'\n})();\n```\n\n#### Creation\n\n##### `new ComponentHTTPClient(url, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates a component HTTP client.\n\n**Parameters:**\n\n* `url`: A string specifying the URL of the component server to connect to.\n* `options`:\n  * `version`: A number specifying the expected version of the component server (default: `undefined`). If a version is specified, an error is thrown when a request is sent and the component server has a different version. The thrown error is a JavaScript `Error` instance with a `code` attribute set to `'COMPONENT_CLIENT_VERSION_DOES_NOT_MATCH_COMPONENT_SERVER_VERSION'`.\n  * `mixins`: An array of the component mixins (e.g., [`Storable`](https://layrjs.com/docs/v1/reference/storable)) to use when constructing the components exposed by the component server (default: `[]`).\n  * `retryFailedRequests`: A boolean or a function returning a boolean specifying whether a request should be retried in case of a network issue (default: `false`). In case a function is specified, the function will receive an object of the shape `{error, numberOfRetries}` where `error` is the error that has occurred and `numberOfRetries` is the number of retries that has been attempted so far. The function can be asynchronous ans should return a boolean.\n  * `maximumRequestRetries`: The maximum number of times a request can be retried (default: `10`).\n  * `minimumTimeBetweenRequestRetries`: A number specifying the minimum time in milliseconds that should elapse between each request retry (default: `3000`).\n\n**Returns:**\n\nA `ComponentHTTPClient` instance.\n\n#### Getting the Served Component\n\n##### `getComponent()` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#get-component-instance-method}\n\nGets the component that is served by the component server.\n\n**Returns:**\n\nA [`Component`](https://layrjs.com/docs/v1/reference/component) class.\n\n**Example:**\n\nSee an [example of use](https://layrjs.com/docs/v1/reference/component-http-client#usage) above."
  },
  {
    "path": "website/frontend/public/docs/v1/reference/component-http-server-5VDR5fS2uD9iTkuHRSywjz.immutable.md",
    "content": "### ComponentHTTPServer <badge type=\"primary\">class</badge> {#component-http-server-class}\n\nA class allowing to serve a root [`Component`](https://layrjs.com/docs/v1/reference/component) so it can be accessed by a [`ComponentHTTPClient`](https://layrjs.com/docs/v1/reference/component-http-client).\n\nThis class provides a basic HTTP server providing one endpoint to serve your root component. If you wish to build an HTTP server providing multiple endpoints, you can use a middleware such as [`component-express-middleware`](https://layrjs.com/docs/v1/reference/component-express-middleware), or implement the necessary plumbing to integrate a [`ComponentServer`](https://layrjs.com/docs/v1/reference/component-server) in your custom HTTP server.\n\n#### Usage\n\nCreate an instance of `ComponentHTTPServer` by specifying the root [`Component`](https://layrjs.com/docs/v1/reference/component) you want to serve, and use the [`start()`](https://layrjs.com/docs/v1/reference/component-http-server#start-instance-method) method to start the server.\n\nSee an example of use in [`ComponentHTTPClient`](https://layrjs.com/docs/v1/reference/component-http-client).\n\n#### Creation\n\n##### `new ComponentHTTPServer(componentOrComponentServer, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates a component HTTP server.\n\n**Parameters:**\n\n* `componentOrComponentServer`: The root [`Component`](https://layrjs.com/docs/v1/reference/component) class to serve. An instance of a [`ComponentServer`](https://layrjs.com/docs/v1/reference/component-server) will be created under the hood. Alternatively, you can pass an existing instance of a [`ComponentServer`](https://layrjs.com/docs/v1/reference/component-server).\n* `options`:\n  * `port`: A number specifying the TCP port to listen to (default: `3333`).\n  * `version`: A number specifying the version of the created [`ComponentServer`](https://layrjs.com/docs/v1/reference/component-server) (default: `undefined`).\n\n**Returns:**\n\nA `ComponentHTTPServer` instance.\n\n#### Methods\n\n##### `start()` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#start-instance-method}\n\nStarts the component HTTP server.\n\n**Example:**\n\n```\nconst server = new ComponentHTTPServer(Movie, {port: 3210});\n\nawait server.start();\n```\n\n##### `stop()` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#stop-instance-method}\n\nStops the component HTTP server.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/component-koa-middleware-402oMlpSmEIARQJLzn4qQg.immutable.md",
    "content": "### component-koa-middleware <badge type=\"primary\">module</badge> {#component-koa-middleware-module}\n\nA [Koa](https://koajs.com/) middleware allowing to serve a root [`Component`](https://layrjs.com/docs/v1/reference/component) so it can be accessed by a [`ComponentHTTPClient`](https://layrjs.com/docs/v1/reference/component-http-client).\n\n#### Usage\n\nCall the [`serveComponent()`](https://layrjs.com/docs/v1/reference/component-koa-middleware#serve-component-function) function to create a middleware for your Koa application.\n\n**Example:**\n\n```\nimport Koa from 'koa';\nimport {Component} from '@layr/component';\nimport {serveComponent} from '@layr/component-koa-middleware';\n\nclass Movie extends Component {\n  // ...\n}\n\nconst app = new Koa();\n\n// Serve the `Movie` component at the root ('/')\napp.use(serveComponent(Movie));\n\napp.listen(3210);\n```\n\nIf you want to serve your component at a specific URL, you can use [`koa-mount`](https://github.com/koajs/mount):\n\n```\nimport mount from 'koa-mount';\n\n// Serve the `Movie` component at a specific URL ('/api')\napp.use(mount('/api', serveComponent(Movie)));\n```\n\n#### Functions\n\n##### `serveComponent(componentOrComponentServer, [options])` <badge type=\"tertiary-outline\">function</badge> {#serve-component-function}\n\nCreates a [Koa](https://koajs.com/) middleware exposing the specified root [`Component`](https://layrjs.com/docs/v1/reference/component) class.\n\n**Parameters:**\n\n* `componentOrComponentServer`: The root [`Component`](https://layrjs.com/docs/v1/reference/component) class to serve. An instance of a [`ComponentServer`](https://layrjs.com/docs/v1/reference/component-server) will be created under the hood. Alternatively, you can pass an existing instance of a [`ComponentServer`](https://layrjs.com/docs/v1/reference/component-server).\n* `options`:\n  * `version`: A number specifying the version of the created [`ComponentServer`](https://layrjs.com/docs/v1/reference/component-server) (default: `undefined`).\n\n**Returns:**\n\nA Koa middleware.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/component-server-e5RUuQXVcCIVLxHiNzCDO.immutable.md",
    "content": "### ComponentServer <badge type=\"primary\">class</badge> {#component-server-class}\n\nA base class allowing to serve a root [`Component`](https://layrjs.com/docs/v1/reference/component) so it can be accessed by a [`ComponentClient`](https://layrjs.com/docs/v1/reference/component-client).\n\nTypically, instead of using this class, you would use a class such as [`ComponentHTTPServer`](https://layrjs.com/docs/v1/reference/component-http-server), or a middleware such as [`component-express-middleware`](https://layrjs.com/docs/v1/reference/component-express-middleware).\n\n#### Creation\n\n##### `new ComponentServer(component, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates a component server.\n\n**Parameters:**\n\n* `component`: The root [`Component`](https://layrjs.com/docs/v1/reference/component) class to serve.\n* `options`:\n  * `version`: A number specifying the version of the returned component server (default: `undefined`).\n\n**Returns:**\n\nA `ComponentServer` instance.\n\n**Example:**\n\nSee [`ComponentClient`'s example](https://layrjs.com/docs/v1/reference/component-client#constructor)."
  },
  {
    "path": "website/frontend/public/docs/v1/reference/embedded-component-4MiZBzspJQbUshU8Q93aJ8.immutable.md",
    "content": "### EmbeddedComponent <badge type=\"primary\">class</badge> {#embedded-component-class}\n\n*Inherits from [`Component`](https://layrjs.com/docs/v1/reference/component).*\n\nThe `EmbeddedComponent` class allows you to define a component that can be embedded into another component. This is useful when you have to deal with a rich data model composed of a hierarchy of properties that can be type checked at runtime and validated. If you don't need such control over some nested attributes, instead of using an embedded component, you can just use an attribute of type `object`.\n\nThe `EmbeddedComponent` class inherits from the [`Component`](https://layrjs.com/docs/v1/reference/component) class, so you can define and consume an embedded component in the same way you would do with any component.\n\nHowever, since an embedded component is owned by its parent component, it doesn't behave like a regular component. Head over [here](https://layrjs.com/docs/v1/reference/component#nesting-components) for a broader explanation.\n\n#### Usage\n\nJust extend the `EmbeddedComponent` class to define a component that has the ability to be embedded.\n\nFor example, a `MovieDetails` embedded component could be defined as follows:\n\n```\n// JS\n\n// movie-details.js\n\nimport {EmbeddedComponent} from '@layr/component';\n\nexport class MovieDetails extends EmbeddedComponent {\n  @attribute('number?') duration;\n  @attribute('string?') aspectRatio;\n}\n```\n\n```\n// TS\n\n// movie-details.ts\n\nimport {EmbeddedComponent} from '@layr/component';\n\nexport class MovieDetails extends EmbeddedComponent {\n  @attribute('number?') duration?: number;\n  @attribute('string?') aspectRatio?: string;\n}\n```\n\nOnce you have defined an embedded component, you can embed it into any other component (a regular component or even another embedded component). For example, here is a `Movie` component that is embedding the `MovieDetails` component:\n\n```\n// JS\n\n// movie.js\n\nimport {Component} from '@layr/component';\n\nimport {MovieDetails} from './movie-details';\n\nclass Movie extends Component {\n  @provide() static MovieDetails = MovieDetails;\n\n  @attribute('string') title;\n  @attribute('MovieDetails') details;\n}\n```\n\n```\n// TS\n\n// movie.ts\n\nimport {Component} from '@layr/component';\n\nimport {MovieDetails} from './movie-details';\n\nclass Movie extends Component {\n  @provide() static MovieDetails = MovieDetails;\n\n  @attribute('string') title!: string;\n  @attribute('MovieDetails') details!: MovieDetails;\n}\n```\n\n> Note that you have to make the `MovieDetails` component accessible from the `Movie` component by using the [`@provide()`](https://layrjs.com/docs/v1/reference/component#provide-decorator) decorator. This way, the `MovieDetails` component can be later referred by its name when you define the `details` attribute using the [`@attribute()`](https://layrjs.com/docs/v1/reference/component#attribute-decorator) decorator.\n\nFinally, the `Movie` component can be instantiated like this:\n\n```\nconst movie = new Movie({\n  title: 'Inception',\n  details: new Movie.MovieDetails({duration: 120, aspectRatio: '16:9'})\n});\n\nmovie.title; // => 'Inception'\nmovie.details.duration; // => 120\n```\n\n#### Methods\n\nSee the methods that are inherited from the [`Component`](https://layrjs.com/docs/v1/reference/component#creation) class.\n\n#### Observability\n\nSee the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v1/reference/observable#observable-class) class.\n\n#### Embeddability\n\n##### `isEmbedded()` <badge type=\"secondary\">class method</badge> {#is-embedded-class-method}\n\nAlways returns `true`.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/identifier-attribute-6Jgrzmrlv4QJoBZVTYYDb9.immutable.md",
    "content": "### IdentifierAttribute <badge type=\"primary\">class</badge> {#identifier-attribute-class}\n\n*Inherits from [`Attribute`](https://layrjs.com/docs/v1/reference/attribute).*\n\nA base class from which [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/primary-identifier-attribute) and [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/secondary-identifier-attribute) are constructed. Unless you build a custom identifier attribute class, you probably won't have to use this class directly.\n\n#### Utilities\n\n##### `isIdentifierAttributeClass(value)` <badge type=\"tertiary-outline\">function</badge> {#is-identifier-attribute-class-function}\n\nReturns whether the specified value is an `IdentifierAttribute` class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isIdentifierAttributeInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-identifier-attribute-instance-function}\n\nReturns whether the specified value is an `IdentifierAttribute` instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/identity-map-01fsjrVv6cSC6awmnNnn6c.immutable.md",
    "content": "### IdentityMap <badge type=\"primary\">class</badge> {#identity-map-class}\n\nA class to manage the instances of the [`Component`](https://layrjs.com/docs/v1/reference/component) classes that are identifiable.\n\nA component class is identifiable when its prototype has a [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/primary-identifier-attribute).\n\nWhen a component class is identifiable, the `IdentityMap` ensures that there can only be one component instance with a specific identifier. So if you try to create two components with the same identifer, you will get an error.\n\n#### Usage\n\nYou shouldn't have to create an `IdentityMap` by yourself. Identity maps are created automatically for each [`Component`](https://layrjs.com/docs/v1/reference/component) class that are identifiable.\n\n**Example:**\n\nHere is a `Movie` component with an `id` primary identifier attribute:\n\n```\n// JS\n\nimport {Component, primaryIdentifier, attribute} from '@layr/component';\n\nclass Movie extends Component {\n  @primaryIdentifier() id;\n  @attribute('string') title;\n}\n```\n\n```\n// TS\n\nimport {Component, primaryIdentifier, attribute} from '@layr/component';\n\nclass Movie extends Component {\n  @primaryIdentifier() id!: string;\n  @attribute('string') title!: string;\n}\n```\n\nTo get the `IdentityMap` of the `Movie` component, simply do:\n\n```\nconst identityMap = Movie.getIdentityMap();\n```\n\nCurrently, the `IdentifyMap` provides only one public method — [`getComponent()`](https://layrjs.com/docs/v1/reference/identity-map#get-component-instance-method) — that allows to retrieve a component instance from its identifier:\n\n```\nconst movie = new Movie({id: 'abc123', title: 'Inception'});\n\nidentityMap.getComponent('abc123'); // => movie\n```\n\n#### Methods\n\n##### `getComponent(identifiers)` <badge type=\"secondary-outline\">instance method</badge> {#get-component-instance-method}\n\nGets a component instance from one of its identifiers. If there are no components corresponding to the specified identifiers, returns `undefined`.\n\n**Parameters:**\n\n* `identifiers`: A plain object specifying some identifiers. The shape of the object should be `{[identifierName]: identifierValue}`. Alternatively, you can specify a string or a number representing the value of the [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/primary-identifier-attribute) of the component you want to get.\n\n**Returns:**\n\nA [`Component`](https://layrjs.com/docs/v1/reference/component) instance or `undefined`.\n\n**Example:**\n\n```\n// JS\n\nimport {Component, primaryIdentifier, secondaryIdentifier} from '@layr/component';\n\nclass Movie extends Component {\n  @primaryIdentifier() id;\n  @secondaryIdentifier() slug;\n}\n\nconst movie = new Movie({id: 'abc123', slug: 'inception'});\n\nMovie.getIdentityMap().getComponent('abc123'); // => movie\nMovie.getIdentityMap().getComponent({id: 'abc123'}); // => movie\nMovie.getIdentityMap().getComponent({slug: 'inception'}); // => movie\nMovie.getIdentityMap().getComponent('xyx456'); // => undefined\n```\n```\n// TS\n\nimport {Component, primaryIdentifier, secondaryIdentifier} from '@layr/component';\n\nclass Movie extends Component {\n  @primaryIdentifier() id!: string;\n  @secondaryIdentifier() slug!: string;\n}\n\nconst movie = new Movie({id: 'abc123', slug: 'inception'});\n\nMovie.getIdentityMap().getComponent('abc123'); // => movie\nMovie.getIdentityMap().getComponent({id: 'abc123'}); // => movie\nMovie.getIdentityMap().getComponent({slug: 'inception'}); // => movie\nMovie.getIdentityMap().getComponent('xyx456'); // => undefined\n```\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/index-1KeXILVBQNLQpRzBhIWjqB.immutable.md",
    "content": "### Index <badge type=\"primary\">class</badge> {#index-class}\n\nRepresents an index for one or several [attributes](https://layrjs.com/docs/v1/reference/attribute) of a [storable component](https://layrjs.com/docs/v1/reference/storable#storable-component-class).\n\nOnce an index is defined for an attribute, all queries involving this attribute (through the [`find()`](https://layrjs.com/docs/v1/reference/storable#find-class-method) or the [`count()`](https://layrjs.com/docs/v1/reference/storable#count-class-method) methods) can be greatly optimized by the storable component's [store](https://layrjs.com/docs/v1/reference/store) and its underlying database.\n\n#### Usage\n\n##### Single Attribute Indexes\n\nTypically, you create an `Index` for a storable component's attribute by using the [`@index()`](https://layrjs.com/docs/v1/reference/storable#index-decorator) decorator. Then, you call the [`migrateStorables()`](https://layrjs.com/docs/v1/reference/store#migrate-storables-instance-method) method on the storable component's store to effectively create the index in the underlying database.\n\nFor example, here is how you would define a `Movie` class with some indexes:\n\n```js\n// JS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute, index} from '@layr/storable';\nimport {MongoDBStore} from '@layr/mongodb-store';\n\nexport class Movie extends Storable(Component) {\n  // Primary and secondary identifier attributes are automatically indexed,\n  // so there is no need to define an index for these types of attributes\n  @primaryIdentifier() id;\n\n  // Let's define an index for the `title` attribute\n  @index() @attribute('string') title;\n}\n\nconst store = new MongoDBStore('mongodb://user:pass@host:port/db');\n\nstore.registerStorable(Movie);\n```\n\n```ts\n// TS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute, index} from '@layr/storable';\nimport {MongoDBStore} from '@layr/mongodb-store';\n\nexport class Movie extends Storable(Component) {\n  // Primary and secondary identifier attributes are automatically indexed,\n  // so there is no need to define an index for these types of attributes\n  @primaryIdentifier() id!: string;\n\n  // Let's define an index for the `title` attribute\n  @index() @attribute('string') title!: string;\n}\n\nconst store = new MongoDBStore('mongodb://user:pass@host:port/db');\n\nstore.registerStorable(Movie);\n```\n\nThen you can call the [`migrateStorables()`](https://layrjs.com/docs/v1/reference/store#migrate-storables-instance-method) method on the store to create the indexes in the MongoDB database:\n\n```\nawait store.migrateStorables();\n```\n\nAnd now that the `title` attribute is indexed, you can make any query on this attribute in a very performant way:\n\n```\nconst movies = await Movie.find({title: 'Inception'});\n```\n\n##### Compound Attribute Indexes\n\nYou can create a compound attribute index to optimize some queries that involve a combination of attributes. To do so, you use the [`@index()`](https://layrjs.com/docs/v1/reference/storable#index-decorator) decorator on the storable component itself:\n\n```js\n// JS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute, index} from '@layr/storable';\nimport {MongoDBStore} from '@layr/mongodb-store';\n\n// Let's define a compound attribute index for the combination of the `year`\n// attribute (descending order) and the `title` attribute (ascending order)\n@index({year: 'desc', title: 'asc'})\nexport class Movie extends Storable(Component) {\n  @primaryIdentifier() id;\n\n  @attribute('string') title;\n\n  @attribute('number') year;\n}\n\nconst store = new MongoDBStore('mongodb://user:pass@host:port/db');\n\nstore.registerStorable(Movie);\n```\n\n```ts\n// TS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute, index} from '@layr/storable';\nimport {MongoDBStore} from '@layr/mongodb-store';\n\n// Let's define a compound attribute index for the combination of the `year`\n// attribute (descending order) and the `title` attribute (ascending order)\n@index({year: 'desc', title: 'asc'})\nexport class Movie extends Storable(Component) {\n  @primaryIdentifier() id!: string;\n\n  @attribute('string') title!: string;\n\n  @attribute('number') year!: number;\n}\n\nconst store = new MongoDBStore('mongodb://user:pass@host:port/db');\n\nstore.registerStorable(Movie);\n```\n\nThen you can call the [`migrateStorables()`](https://layrjs.com/docs/v1/reference/store#migrate-storables-instance-method) method on the store to create the compound attribute index in the MongoDB database:\n\n```\nawait store.migrateStorables();\n```\n\nAnd now you can make any query involving a combination of `year` and `title` in a very performant way:\n\n```\nconst movies = await Movie.find(\n  {year: {$greaterThan: 2010}},\n  true,\n  {sort: {year: 'desc', title: 'asc'}}\n);\n```\n\n#### Creation\n\n##### `new Index(attributes, parent, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates an instance of [`Index`](https://layrjs.com/docs/v1/reference/index).\n\n**Parameters:**\n\n* `attributes`: An object specifying the attributes to be indexed. The shape of the object should be `{attributeName: direction, ...}` where `attributeName` is a string representing the name of an attribute and `direction` is a string representing the sort direction (possible values: `'asc'` or `'desc'`).\n* `parent`: The storable component prototype that owns the index.\n* `options`:\n  * `isUnique`: A boolean specifying whether the index should hold unique values or not (default: `false`). When set to `true`, the underlying database will prevent you to store an attribute with the same value in multiple storable components.\n\n**Returns:**\n\nThe [`Index`](https://layrjs.com/docs/v1/reference/index) instance that was created.\n\n#### Basic Methods\n\n##### `getAttributes()` <badge type=\"secondary-outline\">instance method</badge> {#get-attributes-instance-method}\n\nReturns the indexed attributes.\n\n**Returns:**\n\nAn object of the shape `{attributeName: direction, ...}`.\n\n##### `getParent()` <badge type=\"secondary-outline\">instance method</badge> {#get-parent-instance-method}\n\nReturns the parent of the index.\n\n**Returns:**\n\nA storable component prototype.\n\n#### Utilities\n\n##### `isIndexClass(value)` <badge type=\"tertiary-outline\">function</badge> {#is-index-class-function}\n\nReturns whether the specified value is an `Index` class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isIndexInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-index-instance-function}\n\nReturns whether the specified value is an `Index` instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/memory-router-TGoBOhUXFPp3iDyKA9Sn0.immutable.md",
    "content": "### MemoryRouter <badge type=\"primary\">class</badge> {#memory-router-class}\n\n*Inherits from [`Router`](https://layrjs.com/docs/v1/reference/router).*\n\nA [`Router`](https://layrjs.com/docs/v1/reference/router) that keeps the navigation history in memory. Useful in tests and non-browser environments like [React Native](https://reactnative.dev/).\n\n#### Usage\n\nCreate a `MemoryRouter` instance and register some [routable components](https://layrjs.com/docs/v1/reference/routable#routable-component-class) into it.\n\nSee an example of use in the [`BrowserRouter`](https://layrjs.com/docs/v1/reference/browser-router) class.\n\n#### Creation\n\n##### `new MemoryRouter([options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates a [`MemoryRouter`](https://layrjs.com/docs/v1/reference/memory-router).\n\n**Parameters:**\n\n* `options`:\n  * `initialURLs`: An array of URLs to populate the initial navigation history (default: `[]`).\n  * `initialIndex`: A number specifying the current entry's index in the navigation history (default: the index of the last entry in the navigation history).\n\n**Returns:**\n\nThe [`MemoryRouter`](https://layrjs.com/docs/v1/reference/memory-router) instance that was created.\n\n#### Component Registration\n\nSee the methods that are inherited from the [`Router`](https://layrjs.com/docs/v1/reference/router#component-registration) class.\n\n#### Routes\n\nSee the methods that are inherited from the [`Router`](https://layrjs.com/docs/v1/reference/router#routes) class.\n\n#### Current Location\n\nSee the methods that are inherited from the [`Router`](https://layrjs.com/docs/v1/reference/router#current-location) class.\n\n#### Navigation\n\nSee the methods that are inherited from the [`Router`](https://layrjs.com/docs/v1/reference/router#navigation) class.\n\n#### Observability\n\nSee the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v1/reference/observable#observable-class) class.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/memory-store-78uJRKSOEyr1WnXttCnd9u.immutable.md",
    "content": "### MemoryStore <badge type=\"primary\">class</badge> {#memory-store-class}\n\n*Inherits from [`Store`](https://layrjs.com/docs/v1/reference/store).*\n\nA [`Store`](https://layrjs.com/docs/v1/reference/store) that uses the memory to \"persist\" its registered [storable components](https://layrjs.com/docs/v1/reference/storable#storable-component-class). Since the stored data is wiped off every time the execution environment is restarted, a `MemoryStore` shouldn't be used for a real application.\n\n#### Usage\n\nCreate a `MemoryStore` instance, register some [storable components](https://layrjs.com/docs/v1/reference/storable#storable-component-class) into it, and then use any [`StorableComponent`](https://layrjs.com/docs/v1/reference/storable#storable-component-class)'s method to load, save, delete, or find components from the store.\n\nSee an example of use in the [`MongoDBStore`](https://layrjs.com/docs/v1/reference/mongodb-store) class.\n\n#### Creation\n\n##### `new MemoryStore([options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates a [`MemoryStore`](https://layrjs.com/docs/v1/reference/memory-store).\n\n**Parameters:**\n\n* `options`:\n  * `initialCollections`: A plain object specifying the initial data that should be populated into the store. The shape of the objet should be `{[collectionName]: documents}` where `collectionName` is the name of a [storable component](https://layrjs.com/docs/v1/reference/storable#storable-component-class) class, and `documents` is an array of serialized storable component instances.\n\n**Returns:**\n\nThe [`MemoryStore`](https://layrjs.com/docs/v1/reference/memory-store) instance that was created.\n\n**Example:**\n\n```\n// Create an empty memory store\nconst store = new MemoryStore();\n\n// Create a memory store with some initial data\nconst store = new MemoryStore({\n  User: [\n    {\n      __component: 'User',\n      id: 'xyz789',\n      email: 'user@domain.com'\n    }\n  ],\n  Movie: [\n    {\n      __component: 'Movie',\n      id: 'abc123',\n      title: 'Inception'\n    }\n  ]\n});\n```\n\n#### Component Registration\n\nSee the methods that are inherited from the [`Store`](https://layrjs.com/docs/v1/reference/store#component-registration) class.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/method-3mc2TakviGDKFcdpxFaCIu.immutable.md",
    "content": "### Method <badge type=\"primary\">class</badge> {#method-class}\n\n*Inherits from [`Property`](https://layrjs.com/docs/v1/reference/property).*\n\nA `Method` represents a method of a [Component](https://layrjs.com/docs/v1/reference/component) class, prototype, or instance. It plays the role of a regular JavaScript method, but brings the ability to be exposed to remote calls.\n\n#### Usage\n\nTypically, you define a `Method` using the [`@method()`](https://layrjs.com/docs/v1/reference/component#method-decorator) decorator.\n\nFor example, here is how you would define a `Movie` class with some methods:\n\n```\nimport {Component, method} from '@layr/component';\n\nclass Movie extends Component {\n  // Class method\n  @method() static getConfig() {\n    // ...\n  }\n\n  // Instance method\n  @method() play() {\n    // ...\n  }\n}\n```\n\nThen you can call a method like you would normally do with regular JavaScript:\n\n```\nMovie.getConfig();\n\nconst movie = new Movie({title: 'Inception'});\nmovie.play();\n```\n\nSo far, you may wonder what is the point of defining methods this way. By itself the [`@method()`](https://layrjs.com/docs/v1/reference/component#method-decorator) decorator, except for creating a `Method` instance under the hood, doesn't provide much benefit.\n\nThe trick is that since you have a `Method`, you also have a [`Property`](https://layrjs.com/docs/v1/reference/property) (because `Method` inherits from `Property`), and properties can be exposed to remote access thanks to the [`@expose()`](https://layrjs.com/docs/v1/reference/component#expose-decorator) decorator.\n\nSo here is how you would expose the `Movie` methods:\n\n```\nimport {Component, method} from '@layr/component';\n\nclass Movie extends Component {\n  // Exposed class method\n  @expose({call: true}) @method() static getConfig() {\n    // ...\n  }\n\n  // Exposed instance method\n  @expose({call: true}) @method() play() {\n    // ...\n  }\n}\n```\n\nNow that you have some exposed methods, you can call them remotely in the same way you would do locally:\n\n```\nMovie.getConfig(); // Executed remotely\n\nconst movie = new Movie({title: 'Inception'});\nmovie.play();  // Executed remotely\n```\n\n#### Creation\n\n##### `new Method(name, parent, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates an instance of [`Method`](https://layrjs.com/docs/v1/reference/method). Typically, instead of using this constructor, you would rather use the [`@method()`](https://layrjs.com/docs/v1/reference/component#method-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the method.\n* `parent`: The component class, prototype, or instance that owns the method.\n* `options`:\n  * `exposure`: A [`PropertyExposure`](https://layrjs.com/docs/v1/reference/property#property-exposure-type) object specifying how the method should be exposed to remote calls.\n\n**Returns:**\n\nThe [`Method`](https://layrjs.com/docs/v1/reference/method) instance that was created.\n\n**Example:**\n\n```\nimport {Component, Method} from '@layr/component';\n\nclass Movie extends Component {}\n\nconst play = new Method('play', Movie.prototype, {exposure: {call: true}});\n\nplay.getName(); // => 'play'\nplay.getParent(); // => Movie.prototype\nplay.getExposure(); // => {call: true}\n```\n\n#### Methods\n\nSee the methods that are inherited from the [`Property`](https://layrjs.com/docs/v1/reference/property#basic-methods) class.\n\n#### Utilities\n\n##### `isMethodClass(value)` <badge type=\"tertiary-outline\">function</badge> {#is-method-class-function}\n\nReturns whether the specified value is a `Method` class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isMethodInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-method-instance-function}\n\nReturns whether the specified value is a `Method` instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/mongodb-store-1yTYtEyVy4ZLkF4A2QFaAh.immutable.md",
    "content": "### MongoDBStore <badge type=\"primary\">class</badge> {#mongo-db-store-class}\n\n*Inherits from [`Store`](https://layrjs.com/docs/v1/reference/store).*\n\nA [`Store`](https://layrjs.com/docs/v1/reference/store) that uses a [MongoDB](https://www.mongodb.com/) database to persist its registered [storable components](https://layrjs.com/docs/v1/reference/storable#storable-component-class).\n\n#### Usage\n\nCreate a `MongoDBStore` instance, register some [storable components](https://layrjs.com/docs/v1/reference/storable#storable-component-class) into it, and then use any [`StorableComponent`](https://layrjs.com/docs/v1/reference/storable#storable-component-class)'s method to load, save, delete, or find components from the store.\n\nFor example, let's build a simple `Backend` that provides a `Movie` component.\n\nFirst, let's define the components that we are going to use:\n\n```\n// JS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  @primaryIdentifier() id;\n\n  @attribute() title = '';\n}\n\nclass Backend extends Component {\n  @provide() static Movie = Movie;\n}\n```\n\n```\n// TS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  @primaryIdentifier() id!: string;\n\n  @attribute() title = '';\n}\n\nclass Backend extends Component {\n  @provide() static Movie = Movie;\n}\n```\n\nNext, let's create a `MongoDBStore` instance, and let's register the `Backend` component as the root component of the store:\n\n```\nimport {MongoDBStore} from '@layr/mongodb-store';\n\nconst store = new MongoDBStore('mongodb://user:pass@host:port/db');\n\nstore.registerRootComponent(Backend);\n```\n\nFinally, we can interact with the store by calling some [`StorableComponent`](https://layrjs.com/docs/v1/reference/storable#storable-component-class) methods:\n\n```\nlet movie = new Movie({id: 'abc123', title: 'Inception'});\n\n// Save the movie to the store\nawait movie.save();\n\n// Get the movie from the store\nmovie = await Movie.get('abc123');\nmovie.title; // => 'Inception'\n\n// Modify the movie, and save it to the store\nmovie.title = 'Inception 2';\nawait movie.save();\n\n// Find the movies that have a title starting with 'Inception'\nconst movies = await Movie.find({title: {$startsWith: 'Inception'}});\nmovies.length; // => 1 (one movie found)\nmovies[0].title; // => 'Inception 2'\nmovies[0] === movie; // true (thanks to the identity mapping)\n\n// Delete the movie from the store\nawait movie.delete();\n```\n\n#### Creation\n\n##### `new MongoDBStore(connectionString, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates a [`MongoDBStore`](https://layrjs.com/docs/v1/reference/mongodb-store).\n\n**Parameters:**\n\n* `connectionString`: The [connection string](https://docs.mongodb.com/manual/reference/connection-string/) of the MongoDB database to use.\n* `options`:\n  * `poolSize`: A number specifying the maximum size of the connection pool (default: `1`).\n\n**Returns:**\n\nThe [`MongoDBStore`](https://layrjs.com/docs/v1/reference/mongodb-store) instance that was created.\n\n**Example:**\n\n```\nconst store = new MongoDBStore('mongodb://user:pass@host:port/db');\n```\n\n#### Component Registration\n\nSee the methods that are inherited from the [`Store`](https://layrjs.com/docs/v1/reference/store#component-registration) class.\n\n#### Connection to MongoDB\n\n##### `connect()` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#connect-instance-method}\n\nInitiates a connection to the MongoDB database.\n\nSince this method is called automatically when you interact with the store through any of the [`StorableComponent`](https://layrjs.com/docs/v1/reference/storable#storable-component-class) methods, you shouldn't have to call it manually.\n\n##### `disconnect()` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#disconnect-instance-method}\n\nCloses the connection to the MongoDB database. Unless you are building a tool that uses a store for an ephemeral duration, you shouldn't have to call this method.\n\n#### Migration\n\nSee the methods that are inherited from the [`Store`](https://layrjs.com/docs/v1/reference/store#migration) class.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/observable-6JJJVlFHPe2kPQ4NhsJg7O.immutable.md",
    "content": "### Observable() <badge type=\"primary\">mixin</badge> {#observable-mixin}\n\nBrings observability to any class.\n\nThis mixin is used to construct several Layr's classes such as [`Component`](https://layrjs.com/docs/v1/reference/component) or [`Attribute`](https://layrjs.com/docs/v1/reference/attribute). So, in most cases, you'll have the capabilities provided by this mixin without having to call it.\n\n#### Usage\n\nCall the `Observable()` mixin with any class to construct an [`Observable`](https://layrjs.com/docs/v1/reference/observable#observable-class) class. Then, you can add some observers by using the [`addObserver()`](https://layrjs.com/docs/v1/reference/observable#add-observer-dual-method) method, and trigger their execution anytime by using the [`callObservers()`](https://layrjs.com/docs/v1/reference/observable#call-observers-dual-method) method.\n\nFor example, let's define a `Movie` class using the `Observable()` mixin:\n\n```\n// JS\n\nimport {Observable} from '@layr/observable';\n\nclass Movie extends Observable(Object) {\n  get title() {\n    return this._title;\n  }\n\n  set title(title) {\n    this._title = title;\n    this.callObservers();\n  }\n}\n```\n\n```\n// TS\n\nimport {Observable} from '@layr/observable';\n\nclass Movie extends Observable(Object) {\n  _title?: string;\n\n  get title() {\n    return this._title;\n  }\n\n  set title(title: string) {\n    this._title = title;\n    this.callObservers();\n  }\n}\n```\n\nNext, we can create a `Movie` instance, and observe it:\n\n```\nconst movie = new Movie();\n\nmovie.addObserver(() => {\n  console.log('The movie's title has changed');\n})\n```\n\nAnd now, every time we change the title of `movie`, its observer will be automatically executed:\n\n```\nmovie.title = 'Inception';\n\n// Should display:\n// 'The movie's title has changed'\n```\n\n> Note that the same result could have been achieved by using a Layr [`Component`](https://layrjs.com/docs/v1/reference/component):\n>\n> ```\n> // JS\n>\n> import {Component, attribute} from '@layr/component';\n>\n> class Movie extends Component {\n>   @attribute('string?') title;\n> }\n> ```\n>\n> ```\n> // TS\n>\n> import {Component, attribute} from '@layr/component';\n>\n> class Movie extends Component {\n>   @attribute('string?') title?: string;\n> }\n> ```\n\n### Observable <badge type=\"primary\">class</badge> {#observable-class}\n\nAn `Observable` class is constructed by calling the `Observable()` mixin ([see above](https://layrjs.com/docs/v1/reference/observable#observable-mixin)).\n\n#### Methods\n\n##### `addObserver(observer)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#add-observer-dual-method}\n\nAdds an observer to the current class or instance.\n\n**Parameters:**\n\n* `observer`: A function that will be automatically executed when the [`callObservers()`](https://layrjs.com/docs/v1/reference/observable#call-observers-dual-method) method is called. Alternatively, you can specify an observable for which the observers should be executed, and doing so, you can connect an observable to another observable.\n\n**Example:**\n\n```\nMovie.addObserver(() => {\n  // A `Movie` class observer\n});\n\nconst movie = new Movie();\n\nmovie.addObserver(() => {\n  // A `Movie` instance observer\n});\n\nconst actor = new Actor();\n\n// Connect `actor` to `movie` so that when `callObservers()` is called on `actor`,\n// then `callObservers()` is automatically called on `movie`\nactor.addObserver(movie);\n```\n\n##### `removeObserver(observer)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#remove-observer-dual-method}\n\nRemoves an observer from the current class or instance.\n\n**Parameters:**\n\n* `observer`: A function or a connected observable.\n\n**Example:**\n\n```\nconst observer = () => {\n  // ...\n}\n\n// Add `observer` to the `Movie` class\nMovie.addObserver(observer);\n\n// Remove `observer` from to the `Movie` class\nMovie.removeObserver(observer);\n\nconst movie = new Movie();\nconst actor = new Actor();\n\n// Connect `actor` to `movie`\nactor.addObserver(movie);\n\n// Remove the connection between `actor` and `movie`\nactor.removeObserver(movie);\n```\n\n##### `callObservers([payload])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#call-observers-dual-method}\n\nCalls the observers of the current class or instance.\n\n**Parameters:**\n\n* `payload`: An optional object to pass to the observers when they are executed.\n\n**Example:**\n\n```\nconst movie = new Movie();\n\nmovie.addObserver((payload) => {\n  console.log('Observer called with:', payload);\n});\n\nmovie.callObservers();\n\n// Should display:\n// 'Observer called with: undefined'\n\nmovie.callObservers({changes: ['title']});\n\n// Should display:\n// 'Observer called with: {changes: ['title']}'\n```\n\n#### Bringing Observability to an Object or an Array\n\n##### `createObservable(target)` <badge type=\"tertiary-outline\">function</badge> {#create-observable-function}\n\nReturns an observable from an existing object or array.\n\nThe returned observable is observed deeply. So, for example, if an object contains a nested object, modifying the nested object will trigger the execution of the parent's observers.\n\nThe returned observable provides the same methods as an [`Observable`](https://layrjs.com/docs/v1/reference/observable#observable-class) instance:\n\n- [`addObserver()`](https://layrjs.com/docs/v1/reference/observable#add-observer-dual-method)\n- [`removeObserver()`](https://layrjs.com/docs/v1/reference/observable#remove-observer-dual-method)\n- [`callObservers()`](https://layrjs.com/docs/v1/reference/observable#call-observers-dual-method)\n\n**Parameters:**\n\n* `target`: A JavaScript plain object or array that you want to observe.\n\n**Returns:**\n\nAn observable objet or array.\n\n**Example:**\n\n```\nimport {createObservable} from '@layr/observable';\n\n// Create an observable `movie`\nconst movie = createObservable({\n  title: 'Inception',\n  genres: ['drama'],\n  details: {duration: 120}\n});\n\n// Add an observer\nmovie.addObserver(() => {\n  // ...\n});\n\n// Then, any of the following changes on `movie` will call the observer:\nmovie.title = 'Inception 2';\ndelete movie.title;\nmovie.year = 2010;\nmovie.genres.push('action');\nmovie.genres[1] = 'sci-fi';\nmovie.details.duration = 125;\n```\n\n#### Utilities\n\n##### `isObservable(value)` <badge type=\"tertiary-outline\">function</badge> {#is-observable-function}\n\nReturns whether the specified value is observable. When a value is observable, you can use any the following methods on it: [`addObserver()`](https://layrjs.com/docs/v1/reference/observable#add-observer-dual-method), [`removeObserver()`](https://layrjs.com/docs/v1/reference/observable#remove-observer-dual-method), and [`callObservers()`](https://layrjs.com/docs/v1/reference/observable#call-observers-dual-method).\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/primary-identifier-attribute-6qTkOuHNN4Bq3EjC9JGVqr.immutable.md",
    "content": "### PrimaryIdentifierAttribute <badge type=\"primary\">class</badge> {#primary-identifier-attribute-class}\n\n*Inherits from [`IdentifierAttribute`](https://layrjs.com/docs/v1/reference/identifier-attribute).*\n\nA `PrimaryIdentifierAttribute` is a special kind of attribute that uniquely identify a [Component](https://layrjs.com/docs/v1/reference/component) instance.\n\nA `Component` can have only one `PrimaryIdentifierAttribute`. To define a `Component` with more than one identifier, you can add some [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/secondary-identifier-attribute) in addition to the `PrimaryIdentifierAttribute`.\n\nAnother characteristic of a `PrimaryIdentifierAttribute` is that its value is immutable (i.e., once set it cannot change). This ensures a stable identity of the components across the different layers of an application (e.g., frontend, backend, and database).\n\nWhen a `Component` has a `PrimaryIdentifierAttribute`, its instances are managed by an [`IdentityMap`](https://layrjs.com/docs/v1/reference/identity-map) ensuring that there can only be one instance with a specific identifier.\n\n#### Usage\n\nTypically, you create a `PrimaryIdentifierAttribute` and associate it to a component prototype using the [`@primaryIdentifier()`](https://layrjs.com/docs/v1/reference/component#primary-identifier-decorator) decorator.\n\nFor example, here is how you would define a `Movie` class with an `id` primary identifer attribute:\n\n```\n// JS\n\nimport {Component, primaryIdentifier, attribute} from '@layr/component';\n\nclass Movie extends Component {\n  // An auto-generated 'string' primary identifier attribute\n  @primaryIdentifier() id;\n\n  // A regular attribute\n  @attribute('string') title;\n}\n```\n\n```\n// TS\n\nimport {Component, primaryIdentifier, attribute} from '@layr/component';\n\nclass Movie extends Component {\n  // An auto-generated 'string' primary identifier attribute\n  @primaryIdentifier() id!: string;\n\n  // A regular attribute\n  @attribute('string') title!: string;\n}\n```\n\nThen, to create a `Movie` instance, you would do something like:\n\n```\nconst movie = new Movie({title: 'Inception'});\n\nmovie.id; // => 'ck41vli1z00013h5xx1esffyn'\nmovie.title; // => 'Inception'\n```\n\nNote that we didn't have to specify a value for the `id` attribute; it was automatically generated (using the [`Component.generateId()`](https://layrjs.com/docs/v1/reference/component#generate-id-class-method) method under the hood).\n\nTo create a `Movie` instance with an `id` of your choice, just do:\n\n```\nconst movie = new Movie({id: 'abc123', title: 'Inception'});\n\nmovie.id; // => 'abc123'\nmovie.title; // => 'Inception'\n```\n\nAs mentioned previously, when a component has a primary identifier attribute, all its instances are managed by an [`IdentityMap`](https://layrjs.com/docs/v1/reference/identity-map) ensuring that there is only one instance with a specific identifier.\n\nSo, since we previously created a `Movie` with `'abc123'` as primary identifier, we cannot create another `Movie` with the same primary identifier:\n\n```\nnew Movie({id: 'abc123', title: 'Inception 2'}); // => Error\n```\n\n`PrimaryIdentifierAttribute` values are usually of type `'string'` (the default), but you can also have values of type `'number'`:\n\n```\n// JS\n\nimport {Component, primaryIdentifier} from '@layr/component';\n\nclass Movie extends Component {\n  // An auto-generated 'number' primary identifier attribute\n  @primaryIdentifier('number') id = Math.random();\n}\n```\n\n```\n// TS\n\nimport {Component, primaryIdentifier} from '@layr/component';\n\nclass Movie extends Component {\n  // An auto-generated 'number' primary identifier attribute\n  @primaryIdentifier('number') id = Math.random();\n}\n```\n\n#### Creation\n\n##### `new PrimaryIdentifierAttribute(name, parent, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates an instance of [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/primary-identifier-attribute). Typically, instead of using this constructor, you would rather use the [`@primaryIdentifier()`](https://layrjs.com/docs/v1/reference/component#primary-identifier-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the attribute.\n* `parent`: The component prototype that owns the attribute.\n* `options`:\n  * `valueType`: A string specifying the type of values the attribute can store. It can be either `'string'` or `'number'` (default: `'string'`).\n  * `default`: A function returning the default value of the attribute (default when `valueType` is `'string'`: `function () { return this.constructor.generateId() }`).\n  * `validators`: An array of [validators](https://layrjs.com/docs/v1/reference/validator) for the value of the attribute.\n  * `exposure`: A [`PropertyExposure`](https://layrjs.com/docs/v1/reference/property#property-exposure-type) object specifying how the attribute should be exposed to remote access.\n\n**Returns:**\n\nThe [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/primary-identifier-attribute) instance that was created.\n\n**Example:**\n\n```\nimport {Component, PrimaryIdentifierAttribute} from '@layr/component';\n\nclass Movie extends Component {}\n\nconst id = new PrimaryIdentifierAttribute('id', Movie.prototype);\n\nid.getName(); // => 'id'\nid.getParent(); // => Movie.prototype\nid.getValueType().toString(); // => 'string'\nid.getDefaultValue(); // => function () { return this.constructor.generateId() }`\n```\n\n#### Property Methods\n\nSee the methods that are inherited from the [`Property`](https://layrjs.com/docs/v1/reference/property#basic-methods) class.\n\n#### Attribute Methods\n\nSee the methods that are inherited from the [`Attribute`](https://layrjs.com/docs/v1/reference/attribute#value-type) class.\n\n#### Observability\n\nSee the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v1/reference/observable#observable-class) class.\n\n#### Utilities\n\n##### `isPrimaryIdentifierAttributeClass(value)` <badge type=\"tertiary-outline\">function</badge> {#is-primary-identifier-attribute-class-function}\n\nReturns whether the specified value is a `PrimaryIdentifierAttribute` class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isPrimaryIdentifierAttributeInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-primary-identifier-attribute-instance-function}\n\nReturns whether the specified value is a `PrimaryIdentifierAttribute` instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/property-5bbULbxC52tIPaMAcPEp27.immutable.md",
    "content": "### Property <badge type=\"primary\">class</badge> {#property-class}\n\nA base class from which classes such as [`Attribute`](https://layrjs.com/docs/v1/reference/attribute) or [`Method`](https://layrjs.com/docs/v1/reference/method) are constructed. Unless you build a custom property class, you probably won't have to use this class directly.\n\n#### Creation\n\n##### `new Property(name, parent, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates an instance of [`Property`](https://layrjs.com/docs/v1/reference/property).\n\n**Parameters:**\n\n* `name`: The name of the property.\n* `parent`: The component class, prototype, or instance that owns the property.\n* `options`:\n  * `exposure`: A [`PropertyExposure`](https://layrjs.com/docs/v1/reference/property#property-exposure-type) object specifying how the property should be exposed to remote access.\n\n**Returns:**\n\nThe [`Property`](https://layrjs.com/docs/v1/reference/property) instance that was created.\n\n**Example:**\n\n```\nimport {Component, Property} from '@layr/component';\n\nclass Movie extends Component {}\n\nconst titleProperty = new Property('title', Movie.prototype);\n\ntitleProperty.getName(); // => 'title'\ntitleProperty.getParent(); // => Movie.prototype\n```\n\n#### Basic Methods\n\n##### `getName()` <badge type=\"secondary-outline\">instance method</badge> {#get-name-instance-method}\n\nReturns the name of the property.\n\n**Returns:**\n\nA string.\n\n**Example:**\n\n```\ntitleProperty.getName(); // => 'title'\n```\n\n##### `getParent()` <badge type=\"secondary-outline\">instance method</badge> {#get-parent-instance-method}\n\nReturns the parent of the property.\n\n**Returns:**\n\nA component class, prototype, or instance.\n\n**Example:**\n\n```\ntitleProperty.getParent(); // => Movie.prototype\n```\n\n#### Exposure\n\n##### `getExposure()` <badge type=\"secondary-outline\">instance method</badge> {#get-exposure-instance-method}\n\nReturns an object specifying how the property is exposed to remote access.\n\n**Returns:**\n\nA [`PropertyExposure`](https://layrjs.com/docs/v1/reference/property#property-exposure-type) object.\n\n**Example:**\n\n```\ntitleProperty.getExposure(); // => {get: true, set: true}\n```\n\n##### `setExposure([exposure])` <badge type=\"secondary-outline\">instance method</badge> {#set-exposure-instance-method}\n\nSets how the property is exposed to remote access.\n\n**Parameters:**\n\n* `exposure`: A [`PropertyExposure`](https://layrjs.com/docs/v1/reference/property#property-exposure-type) object.\n\n**Example:**\n\n```\ntitleProperty.setExposure({get: true, set: true});\n```\n\n##### `operationIsAllowed(operation)` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">possibly async</badge> {#operation-is-allowed-instance-method}\n\nReturns whether an operation is allowed on the property.\n\n**Parameters:**\n\n* `operation`: A string representing an operation. Currently supported operations are 'get', 'set', and 'call'.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\ntitleProperty.operationIsAllowed('get'); // => true\ntitleProperty.operationIsAllowed('call'); // => false\n```\n\n##### `PropertyExposure` <badge type=\"primary-outline\">type</badge> {#property-exposure-type}\n\nA `PropertyExposure` is a plain object specifying how a property is exposed to remote access.\n\nThe shape of the object is `{[operation]: permission}` where:\n\n- `operation` is a string representing the different types of operations (`'get'` and `'set'` for attributes, and `'call'` for methods).\n- `permission` is a boolean (or a string or array of strings if the [`WithRoles`](https://layrjs.com/docs/v1/reference/with-roles) mixin is used) specifying whether the operation is allowed or not.\n\n**Example:**\n\n```\n{get: true, set: true}\n{get: 'anyone', set: ['author', 'admin']}\n{call: true}\n{call: 'admin'}\n```\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/query-Q04JuSZAcx8HGvYktmTlI.immutable.md",
    "content": "### Query <badge type=\"primary-outline\">type</badge> {#query-type}\n\nA plain object specifying the criteria to be used when selecting some components from a store with the methods [`StorableComponent.find()`](https://layrjs.com/docs/v1/reference/storable#find-class-method) or [`StorableComponent.count()`](https://layrjs.com/docs/v1/reference/storable#count-class-method).\n\n#### Basic Queries\n\n##### Empty Query\n\nSpecify an empty object (`{}`) to select all the components:\n\n```\n// Find all the movies\nawait Movie.find({});\n```\n\n##### Single Attribute Query\n\nSpecify an object composed of an attribute's name and value to select the components that have an attribute's value equals to a specific value:\n\n```\n// Find the Japanese movies\nawait Movie.find({country: 'Japan'});\n\n// Find the unreleased movies\nawait Movie.find({year: undefined});\n```\n\n##### Multiple Attributes Query\n\nYou can combine several attributes' name and value to select the components by multiple attributes:\n\n```\n// Find the Japanese drama movies\nawait Movie.find({country: 'Japan', genre: 'drama'});\n```\n\n#### Basic Operators\n\nInstead of a specific value, you can specify an object containing one or more operators to check whether the value of an attribute matches certain criteria.\n\n##### `$equal`\n\nUse the `$equal` operator to check whether the value of an attribute is equal to a specific value. This is the default operator, so when you specify a value without any operator, the `$equal` operator is used under the hood:\n\n```\n// Find the Japanese movies\nawait Movie.find({country: {$equal: 'Japan'}});\n\n// Same as above, but in a short manner\nawait Movie.find({country: 'Japan'});\n```\n\n##### `$notEqual`\n\nUse the `$notEqual` operator to check whether the value of an attribute is different than a specific value:\n\n```\n// Find the non-Japanese movies\nawait Movie.find({country: {$notEqual: 'Japan'}});\n\n// Find the released movies\nawait Movie.find({year: {$notEqual: undefined}});\n```\n\n##### `$greaterThan`\n\nUse the `$greaterThan` operator to check whether the value of an attribute is greater than a specific value:\n\n```\n// Find the movies released after 2010\nawait Movie.find({year: {$greaterThan: 2010}});\n```\n\n##### `$greaterThanOrEqual`\n\nUse the `$greaterThanOrEqual` operator to check whether the value of an attribute is greater than or equal to a specific value:\n\n```\n// Find the movies released in or after 2010\nawait Movie.find({year: {$greaterThanOrEqual: 2010}});\n```\n\n##### `$lessThan`\n\nUse the `$lessThan` operator to check whether the value of an attribute is less than a specific value:\n\n```\n// Find the movies released before 2010\nawait Movie.find({year: {$lessThan: 2010}});\n```\n\n##### `$lessThanOrEqual`\n\nUse the `$lessThanOrEqual` operator to check whether the value of an attribute is less than or equal to a specific value:\n\n```\n// Find the movies released in or before 2010\nawait Movie.find({year: {$lessThanOrEqual: 2010}});\n```\n\n##### `$in`\n\nUse the `$in` operator to check whether the value of an attribute is equal to any value in the specified array:\n\n```\n// Find the movies that are Japanese or French\nawait Movie.find({country: {$in: ['Japan', 'France']}});\n\n// Find the movies that have any of the specified identifiers\nawait Movie.find({id: {$in: ['abc123', 'abc456', 'abc789']}});\n```\n\n##### Combining several operators\n\nYou can combine several operators to check whether the value of an attribute matches several criteria:\n\n```\n// Find the movies released after 2010 and before 2015\nawait Movie.find({year: {$greaterThan: 2010, $lessThan: 2015}});\n```\n\n#### String Operators\n\nA number of operators are dedicated to string attributes.\n\n##### `$includes`\n\nUse the `$includes` operator to check whether the value of a string attribute includes a specific string:\n\n```\n// Find the movies that have the string 'awesome' in their title\nawait Movie.find({title: {$includes: 'awesome'}});\n```\n\n##### `$startsWith`\n\nUse the `$startsWith` operator to check whether the value of a string attribute starts with a specific string:\n\n```\n// Find the movies that have their title starting with the string 'awesome'\nawait Movie.find({title: {$startsWith: 'awesome'}});\n```\n\n##### `$endsWith`\n\nUse the `$endsWith` operator to check whether the value of a string attribute ends with a specific string:\n\n```\n// Find the movies that have their title ending with the string 'awesome'\nawait Movie.find({title: {$endsWith: 'awesome'}});\n```\n\n##### `$matches`\n\nUse the `$matches` operator to check whether the value of a string attribute matches the specified [regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions):\n\n```\n// Find the movies that have a number in their title\nawait Movie.find({title: {$matches: /\\d/}});\n```\n\n#### Array Operators\n\nA number of operators are dedicated to array attributes.\n\n##### `$some`\n\nUse the `$some` operator to check whether an array attribute has an item equals to a specific value. This is the default operator for array attributes, so when you specify a value without any array operator, the `$some` operator is used under the hood:\n\n```\n// Find the movies that have the 'awesome' tag\nawait Movie.find({tags: {$some: 'awesome'}});\n\n// Same as above, but in a short manner\nawait Movie.find({tags: 'awesome'});\n```\n\n##### `$every`\n\nUse the `$every` operator to check whether an array attribute has all its items equals to a specific value:\n\n```\n// Find the movies that have all their tags equal to 'awesome'\nawait Movie.find({tags: {$every: 'awesome'}});\n```\n\n##### `$length`\n\nUse the `$length` operator to check whether an array attribute has a specific number of items:\n\n```\n// Find the movies that have three tags:\nawait Movie.find({tags: {$length: 3}});\n\n// Find the movies that don't have any tag:\nawait Movie.find({tags: {$length: 0}});\n```\n\n#### Logical Operators\n\nThe logical operators allows you to combine several subqueries.\n\n##### `$and`\n\nUse the `$and` operator to perform a logical **AND** operation on an array of subqueries and select the components that satisfy *all* the subqueries. Note that since **AND** is the implicit logical operation when you combine multiple attributes or operators, you will typically use the `$and` operator in combination with some other logical operators such as [`$or`](https://layrjs.com/docs/v1/reference/query#or) to avoid repetition.\n\n```\n// Find the Japanese drama movies\nawait Movie.find({$and: [{country: 'Japan'}, {genre: 'drama'}]});\n\n// Same as above, but in a short manner\nawait Movie.find({country: 'Japan', genre: 'drama'});\n\n// Find the movies released after 2010 and before 2015\nawait Movie.find({$and: [{year: {$greaterThan: 2010}}, {year: {$lessThan: 2015}}]});\n\n// Same as above, but in a short manner\nawait Movie.find({year: {$greaterThan: 2010, $lessThan: 2015}});\n\n// Find the Japanese movies released before 2010 or after 2015\nawait Movie.find({\n  $and: [\n    {country: 'Japan'},\n    {$or: [{year: {$lessThan: 2010}}, {year: {$greaterThan: 2015}}]}\n  ]\n});\n\n// Same as above, but we have to repeat the country to remove the $and operator\nawait Movie.find({\n  $or: [\n    {country: 'Japan', year: {$lessThan: 2010}},\n    {country: 'Japan', year: {$greaterThan: 2015}}\n  ]\n});\n```\n\n##### `$or`\n\nUse the `$or` operator to perform a logical **OR** operation on an array of subqueries and select the components that satisfy *at least* one of the subqueries.\n\n```\n// Find the movies that are either Japanese or a drama\nawait Movie.find({$or: [{country: 'Japan', {genre: 'drama'}]});\n\n// Find the movies released before 2010 or after 2015\nawait Movie.find({$or: [{year: {$lessThan: 2010}}, {year: {$greaterThan: 2015}}]});\n```\n\n##### `$nor`\n\nUse the `$nor` operator to perform a logical **NOR** operation on an array of subqueries and select the components that *fail all* the subqueries.\n\n```\n// Find the movies that are not Japanese and not a drama\nawait Movie.find({$nor: [{country: 'Japan', {genre: 'drama'}]});\n```\n\n##### `$not`\n\nUse the `$not` operator to invert the effect of an operator.\n\n```\n// Find the non-Japanese movies\nawait Movie.find({country: {$not: {$equal: 'Japan'}}});\n\n// Same as above, but in a short manner\nawait Movie.find({country: {$notEqual: 'Japan'}});\n\n// Find the movies that was not released in or after 2010\nawait Movie.find({year: {$not: {$greaterThanOrEqual: 2010}}});\n\n// Same as above, but in a short manner\nawait Movie.find({year: {$lessThan: 2010}});\n```\n\n#### Embedded Components\n\nWhen a query involves an [embedded component](https://layrjs.com/docs/v1/reference/embedded-component), wrap the attributes of the embedded component in an object:\n\n```\n// Find the movies that have a '16:9' aspect ratio\nawait Movie.find({details: {aspectRatio: '16:9'}});\n\n// Find the movies that have a '16:9' aspect ratio and are longer than 2 hours\nawait Movie.find({details: {aspectRatio: '16:9', duration: {$greaterThan: 120}}});\n```\n\n#### Referenced Components\n\nTo check whether a component holds a [reference to another component](https://layrjs.com/docs/v1/reference/component#referencing-components), you can specify an object representing the [primary identifier](https://layrjs.com/docs/v1/reference/primary-identifier-attribute) of the referenced component:\n\n```\n// Find the Tarantino's movies\nconst tarantino = await Director.get({slug: 'quentin-tarantino'});\nawait Movie.find({director: {id: tarantino.id}});\n```\n\nWherever you can specify a primary identifier, you can specify a component instead. So, the example above can be shortened as follows:\n\n```\n// Find the Tarantino's movies in a short manner\nconst tarantino = await Director.get({slug: 'quentin-tarantino'});\nawait Movie.find({director: tarantino});\n```\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/react-integration-3EgAB99IhnithzdqiytvwF.immutable.md",
    "content": "### react-integration <badge type=\"primary\">module</badge> {#react-integration-module}\n\nProvides a decorator and a number of hooks to simplify the use of [React](https://reactjs.org/) inside a Layr [component](https://layrjs.com/docs/v1/reference/component).\n\n#### Decorators\n\n##### `@view()` <badge type=\"tertiary\">decorator</badge> {#view-decorator}\n\nDecorates a method of a Layr [component](https://layrjs.com/docs/v1/reference/component) so it be can used as a React component.\n\nLike any React component, the method can receive some properties as first parameter and return some [React elements](https://reactjs.org/docs/rendering-elements.html) to render (or `null` if it doesn't render anything).\n\nThe decorator binds the method to a specific component, so when the method is executed by React (via, for example, a reference included in a [JSX expression](https://reactjs.org/docs/introducing-jsx.html)), it has access to the bound component through `this`.\n\nAlso, the decorator observes the attributes of the bound component, so when the value of an attribute changes, the React component is automatically re-rendered.\n\n**Example:**\n\n```\nimport {Component, attribute} from '@layr/component';\nimport React from 'react';\nimport ReactDOM from 'react-dom';\nimport {view} from '@layr/react-integration';\n\nclass Person extends Component {\n  @attribute('string') firstName = '';\n\n  @attribute('string') lastName = '';\n\n  @view() FullName() {\n    return <span>{`${this.firstName} ${this.fullName}`}</span>;\n  }\n}\n\nconst person = new Person({firstName: 'Alan', lastName: 'Turing'});\n\nReactDOM.render(<person.FullName />, document.getElementById('root'));\n```\n\n#### Hooks\n\n##### `useBrowserRouter(rootComponent)` <badge type=\"tertiary-outline\">react hook</badge> {#use-browser-router-react-hook}\n\nCreates a [`BrowserRouter`](https://layrjs.com/docs/v1/reference/browser-router) and registers the specified root [component](https://layrjs.com/docs/v1/reference/component).\n\nTypically, this hook is used in the context of a \"view method\" (i.e., a component method decorated by the [`@view()`](https://layrjs.com/docs/v1/reference/react-integration#view-decorator) decorator) at the root of an application.\n\nThe created router is observed so the view where this hook is used is automatically re-rendered when the current route changes.\n\n**Parameters:**\n\n* `rootComponent`: A [`Component`](https://layrjs.com/docs/v1/reference/component) class providing some [routable components](https://layrjs.com/docs/v1/reference/routable#routable-component-class).\n\n**Returns:**\n\nAn array of the shape `[router, isReady]` where `router` is the [`BrowserRouter`](https://layrjs.com/docs/v1/reference/browser-router) instance that was created and `isReady` is a boolean indicating whether the router is ready. Since the router is initialized asynchronously, make sure that the value of `isReady` is `true` before consuming `router`.\n\n**Example:**\n\n```\nimport {Component, provide} from '@layr/component';\nimport React from 'react';\nimport {view, useBrowserRouter} from '@layr/react-integration';\n\nimport {MyComponent} from './my-component';\n\nclass Frontend extends Component {\n  @provide() static MyComponent = MyComponent; // A routable component\n\n  @view() static View() {\n    const [router, isReady] = useBrowserRouter(this);\n\n    if (!isReady) {\n      return null;\n    }\n\n    return (\n      <div>\n        <h1>My App</h1>\n        {router.callCurrentRoute()}\n      </div>\n    );\n  }\n}\n```\n\n##### `useMemoryRouter(rootComponent, [options])` <badge type=\"tertiary-outline\">react hook</badge> {#use-memory-router-react-hook}\n\nCreates a [`MemoryRouter`](https://layrjs.com/docs/v1/reference/memory-router) and registers the specified root [component](https://layrjs.com/docs/v1/reference/component).\n\nTypically, this hook is used in the context of a \"view method\" (i.e., a component method decorated by the [`@view()`](https://layrjs.com/docs/v1/reference/react-integration#view-decorator) decorator) at the root of an application.\n\nThe created router is observed so the view where this hook is used is automatically re-rendered when the current route changes.\n\n**Parameters:**\n\n* `rootComponent`: A [`Component`](https://layrjs.com/docs/v1/reference/component) class providing some [routable components](https://layrjs.com/docs/v1/reference/routable#routable-component-class).\n* `options`:\n  * `initialURLs`: An array of URLs to populate the initial navigation history (default: `[]`).\n  * `initialIndex`: A number specifying the current entry's index in the navigation history (default: the index of the last entry in the navigation history).\n\n**Returns:**\n\nAn array of the shape `[router]` where `router` is the [`MemoryRouter`](https://layrjs.com/docs/v1/reference/memory-router) instance that was created.\n\n**Example:**\n\n```\nimport {Component, provide} from '@layr/component';\nimport React from 'react';\nimport {view, useMemoryRouter} from '@layr/react-integration';\n\nimport {MyComponent} from './my-component';\n\nclass Frontend extends Component {\n  @provide() static MyComponent = MyComponent; // A routable component\n\n  @view() static View() {\n    const [router] = useMemoryRouter(this, {initialURLs: ['/']});\n\n    return (\n      <div>\n        <h1>My App</h1>\n        {router.callCurrentRoute()}\n      </div>\n    );\n  }\n}\n```\n\n##### `useObserve(observable)` <badge type=\"tertiary-outline\">react hook</badge> {#use-observe-react-hook}\n\nMakes a view dependent of an [observable](https://layrjs.com/docs/v1/reference/observable#observable-type) so the view is automatically re-rendered when the observable changes.\n\n**Parameters:**\n\n* `observable`: An [observable](https://layrjs.com/docs/v1/reference/observable#observable-type) object.\n\n**Example:**\n\n```\nimport {Component} from '@layr/component';\nimport {createObservable} from '@layr/observable';\nimport React from 'react';\nimport {view, useObserve} from '@layr/react-integration';\n\nconst observableArray = createObservable([]);\n\nclass MyComponent extends Component {\n  @view() static View() {\n    useObserve(observableArray);\n\n    return (\n      <div>\n        {`observableArray's length: ${observableArray.length}`}\n      </div>\n    );\n  }\n}\n\n// Changing `observableArray` will re-render `MyComponent.View()`\nobservableArray.push('abc');\n```\n\n##### `useAsyncCallback(asyncCallback, dependencies)` <badge type=\"tertiary-outline\">react hook</badge> {#use-async-callback-react-hook}\n\nAllows you to define an asynchronous callback and keep track of its execution.\n\nPlays the same role as the React built-in [`useCallback()`](https://reactjs.org/docs/hooks-reference.html#usecallback) hook but works with asynchronous callbacks.\n\n**Parameters:**\n\n* `asyncCallback`: An asynchronous callback.\n* `dependencies`: An array of values on which the asynchronous callback depends (default: `[]`).\n\n**Returns:**\n\nAn array of the shape `[trackedCallback, isExecuting, error, result]` where `trackedCallback` is a function that you can call to execute the asynchronous callback, `isExecuting` is a boolean indicating whether the asynchronous callback is being executed, `error` is the error thrown by the asynchronous callback in case of failed execution, and `result` is the value returned by the asynchronous callback in case of succeeded execution.\n\n**Example:**\n\n```\nimport {Component} from '@layr/component';\nimport React from 'react';\nimport {view, useAsyncCallback} from '@layr/react-integration';\n\nclass Article extends Component {\n  @view() UpvoteButton() {\n    const [handleUpvote, isUpvoting, upvotingError] = useAsyncCallback(async () => {\n      await this.upvote();\n    });\n\n    return (\n      <div>\n        <button onClick={handleUpvote} disabled={isUpvoting}>Upvote</button>\n        {upvotingError && ' An error occurred while upvoting the article.'}\n      </div>\n    );\n  }\n}\n```\n\n##### `useAsyncMemo(asyncFunc, dependencies)` <badge type=\"tertiary-outline\">react hook</badge> {#use-async-memo-react-hook}\n\nMemoizes the result of an asynchronous function execution and provides a \"recompute function\" that you can call to recompute the memoized result.\n\nThe asynchronous function is executed one time when the React component is rendered for the first time, and each time a dependency is changed or the \"recompute function\" is called.\n\nPlays the same role as the React built-in [`useMemo()`](https://reactjs.org/docs/hooks-reference.html#usememo) hook but works with asynchronous functions and allows to recompute the memoized result.\n\n**Parameters:**\n\n* `asyncFunc`: An asynchronous function to compute the memoized result.\n* `dependencies`: An array of values on which the memoized result depends (default: `[]`, which means that the memoized result will be recomputed only when the \"recompute function\" is called).\n\n**Returns:**\n\nAn array of the shape `[memoizedResult, isExecuting, error, recompute]` where `memoizedResult` is the result returned by the asynchronous function in case of succeeded execution, `isExecuting` is a boolean indicating whether the asynchronous function is being executed, `error` is the error thrown by the asynchronous function in case of failed execution, and `recompute` is a function that you can call to recompute the memoized result.\n\n**Example:**\n\n```\nimport {Component} from '@layr/component';\nimport {Storable} from '@layr/storable';\nimport React from 'react';\nimport {view, useAsyncMemo} from '@layr/react-integration';\n\nclass Article extends Storable(Component) {\n  // ...\n\n  @view() static List() {\n    const [articles, isLoading, loadingError, retryLoading] = useAsyncMemo(\n      async () => {\n        return await this.find();\n      }\n    );\n\n    if (isLoading) {\n      return <div>Loading the articles...</div>;\n    }\n\n    if (loadingError) {\n      return (\n        <div>\n          An error occurred while loading the articles.\n          <button onClick={retryLoading}>Retry</button>\n        </div>\n      );\n    }\n\n    return articles.map((article) => (\n      <div key={article.id}>{article.title}</div>\n    ));\n  }\n}\n```\n\n##### `useRecomputableMemo(func, dependencies)` <badge type=\"tertiary-outline\">react hook</badge> {#use-recomputable-memo-react-hook}\n\nMemoizes the result of a function execution and provides a \"recompute function\" that you can call to recompute the memoized result.\n\nThe function is executed one time when the React component is rendered for the first time, and each time a dependency is changed or the \"recompute function\" is called.\n\nPlays the same role as the React built-in [`useMemo()`](https://reactjs.org/docs/hooks-reference.html#usememo) hook but with the extra ability to recompute the memoized result.\n\n**Parameters:**\n\n* `func`: A function to compute the memoized result.\n* `dependencies`: An array of values on which the memoized result depends (default: `[]`, which means that the memoized result will be recomputed only when the \"recompute function\" is called).\n\n**Returns:**\n\nAn array of the shape `[memoizedResult, recompute]` where `memoizedResult` is the result of the function execution, and `recompute` is a function that you can call to recompute the memoized result.\n\n**Example:**\n\n```\nimport {Component, provide} from '@layr/component';\nimport {Storable} from '@layr/storable';\nimport React, {useCallback} from 'react';\nimport {view, useRecomputableMemo} from '@layr/react-integration';\n\nclass Article extends Storable(Component) {\n  // ...\n}\n\nclass Blog extends Component {\n  @provide() static Article = Article;\n\n  @view() static ArticleCreator() {\n    const [article, resetArticle] = useRecomputableMemo(() => new Article());\n\n    const createArticle = useCallback(async () => {\n      await article.save();\n      resetArticle();\n    }, [article]);\n\n    return (\n      <div>\n        <article.CreateForm onSubmit={createArticle} />\n      </div>\n    );\n  }\n}\n```\n\n##### `useAsyncCall(asyncFunc, dependencies)` <badge type=\"tertiary-outline\">react hook</badge> {#use-async-call-react-hook}\n\nAllows you to call an asynchronous function and keep track of its execution.\n\nThe function is executed one time when the React component is rendered for the first time, and each time a dependency is changed or the \"recall function\" is called.\n\n**Parameters:**\n\n* `asyncFunc`: The asynchronous function to call.\n* `dependencies`: An array of values on which the asynchronous function depends (default: `[]`, which means that the asynchronous will be recalled only when the \"recall function\" is called).\n\n**Returns:**\n\nAn array of the shape `[isExecuting, error, recall]` where `isExecuting` is a boolean indicating whether the asynchronous function is being executed, `error` is the error thrown by the asynchronous function in case of failed execution, and `recall` is a function that you can call to recall the asynchronous function.\n\n**Example:**\n\n```\n// JS\n\nimport {Component, provide, attribute} from '@layr/component';\nimport {Storable} from '@layr/storable';\nimport React from 'react';\nimport {view, useAsyncCall} from '@layr/react-integration';\n\nclass Article extends Storable(Component) {\n  // ...\n}\n\nclass Blog extends Component {\n  @provide() static Article = Article;\n\n  @attribute('Article[]?') static loadedArticles;\n\n  @view() static View() {\n    const [isLoading, loadingError, retryLoading] = useAsyncCall(\n      async () => {\n        this.loadedArticles = await this.Article.find();\n      }\n    );\n\n    if (isLoading) {\n      return <div>Loading the articles...</div>;\n    }\n\n    if (loadingError) {\n      return (\n        <div>\n          An error occurred while loading the articles.\n          <button onClick={retryLoading}>Retry</button>\n        </div>\n      );\n    }\n\n    return this.loadedArticles.map((article) => (\n      <div key={article.id}>{article.title}</div>\n    ));\n  }\n}\n```\n```\n// TS\n\nimport {Component, provide, attribute} from '@layr/component';\nimport {Storable} from '@layr/storable';\nimport React from 'react';\nimport {view, useAsyncCall} from '@layr/react-integration';\n\nclass Article extends Storable(Component) {\n  // ...\n}\n\nclass Blog extends Component {\n  @provide() static Article = Article;\n\n  @attribute('Article[]?') static loadedArticles?: Article[];\n\n  @view() static View() {\n    const [isLoading, loadingError, retryLoading] = useAsyncCall(\n      async () => {\n        this.loadedArticles = await this.Article.find();\n      }\n    );\n\n    if (isLoading) {\n      return <div>Loading the articles...</div>;\n    }\n\n    if (loadingError) {\n      return (\n        <div>\n          An error occurred while loading the articles.\n          <button onClick={retryLoading}>Retry</button>\n        </div>\n      );\n    }\n\n    return this.loadedArticles!.map((article) => (\n      <div key={article.id}>{article.title}</div>\n    ));\n  }\n}\n```\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/role-UodTLJi6Lg6UPXOKZdp8K.immutable.md",
    "content": "### Role <badge type=\"primary\">class</badge> {#role-class}\n\nRepresents a role in a [`ComponentWithRoles`](https://layrjs.com/docs/v1/reference/with-roles#component-with-roles-class) class or prototype.\n\nA role is composed of:\n\n- A name.\n- A parent which should be a [`ComponentWithRoles`](https://layrjs.com/docs/v1/reference/with-roles#component-with-roles-class) class or prototype.\n- A resolver which should be a function returning a boolean indicating whether a user has the corresponding role.\n\n#### Usage\n\nTypically, you create a `Role` by using the [`@role()`](https://layrjs.com/docs/v1/reference/with-roles#role-decorator) decorator.\n\nSee an example of use in the [`WithRoles()`](https://layrjs.com/docs/v1/reference/with-roles#with-roles-mixin) mixin.\n\n#### Creation\n\n##### `new Role(name, parent, resolver)` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates an instance of [`Role`](https://layrjs.com/docs/v1/reference/role).\n\nTypically, instead of using this constructor, you would rather use the [`@role()`](https://layrjs.com/docs/v1/reference/with-roles#role-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the role.\n* `parent`: The parent of the role which should be a [`ComponentWithRoles`](https://layrjs.com/docs/v1/reference/with-roles#component-with-roles-class) class or prototype.\n* `resolver`: A function that should return a boolean indicating whether a user has the corresponding role. The function can be asynchronous and is called with the current class or instance as `this` context.\n\n**Returns:**\n\nThe [`Role`](https://layrjs.com/docs/v1/reference/role) instance that was created.\n\n**Example:**\n\n```\nconst role = new Role('admin', Article, function () {\n // ...\n});\n```\n\n#### Methods\n\n##### `getName()` <badge type=\"secondary-outline\">instance method</badge> {#get-name-instance-method}\n\nReturns the name of the role.\n\n**Returns:**\n\nA string.\n\n##### `getParent()` <badge type=\"secondary-outline\">instance method</badge> {#get-parent-instance-method}\n\nReturns the parent of the role.\n\n**Returns:**\n\nA [`ComponentWithRoles`](https://layrjs.com/docs/v1/reference/with-roles#component-with-roles-class) class or instance.\n\n##### `getResolver()` <badge type=\"secondary-outline\">instance method</badge> {#get-resolver-instance-method}\n\nReturns the resolver function of the role.\n\n**Returns:**\n\nA function.\n\n##### `resolve()` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">possibly async</badge> {#resolve-instance-method}\n\nResolves the role by calling its resolver function.\n\nThe resolver function is called with the role's parent as `this` context.\n\nOnce a role has been resolved, the result is cached, so the resolver function is called only one time.\n\n**Returns:**\n\nA boolean.\n\n#### Utilities\n\n##### `isRoleInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-role-instance-function}\n\nReturns whether the specified value is a [`Role`](https://layrjs.com/docs/v1/reference/role) instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `assertIsRoleInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#assert-is-role-instance-function}\n\nThrows an error if the specified value is not a [`Role`](https://layrjs.com/docs/v1/reference/role) instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/routable-3zt4vMLvzQR3aqOwoWd3xV.immutable.md",
    "content": "### Routable() <badge type=\"primary\">mixin</badge> {#routable-mixin}\n\nExtends a [`Component`](https://layrjs.com/docs/v1/reference/component) class with some routing capabilities.\n\n#### Usage\n\nCall `Routable()` with a [`Component`](https://layrjs.com/docs/v1/reference/component) class to construct a [`RoutableComponent`](https://layrjs.com/docs/v1/reference/routable#routable-component-class) class. Then, you can define some routes into this class by using the [`@route()`](https://layrjs.com/docs/v1/reference/routable#route-decorator) decorator.\n\n**Example:**\n\n```\n// JS\n\nimport {Component} from '@layr/component';\nimport {Routable, route} from '@layr/routable';\n\nclass Article extends Routable(Component) {\n  @route('/articles/:id/upvote') static upvote({id}) {\n    // ...\n  }\n}\n```\n\n```\n// TS\n\nimport {Component} from '@layr/component';\nimport {Routable, route} from '@layr/routable';\n\nclass Article extends Routable(Component) {\n  @route('/articles/:id/upvote') static upvote({id}: {id: string}) {\n    // ...\n  }\n}\n```\n\nOnce you have a routable component, you can use any method provided by the `Routable()` mixin.\n\nFor example, to call the `upvote()` method by a URL, you can use the [`callRouteByURL()`](https://layrjs.com/docs/v1/reference/routable#call-route-by-url-class-method) method:\n\n```\nawait Article.callRouteByURL('/articles/abc123/upvote');\n\n// Which is the equivalent of calling the `upvote()` method directly:\nawait Article.upvote({id: 'abc123'});\n```\n\nA routable component can be registered into a router such as [BrowserRouter](https://layrjs.com/docs/v1/reference/browser-router) by using the [`registerRoutable()`](https://layrjs.com/docs/v1/reference/router#register-routable-instance-method) method (or [`registerRootComponent()`](https://layrjs.com/docs/v1/reference/router#register-root-component-instance-method) to register several components at once):\n\n```\nimport {BrowserRouter} from '@layr/browser-router';\n\nconst router = new BrowserRouter();\n\nrouter.registerRoutable(Article);\n```\n\nOnce a routable component is registered into a router you can control it through its router:\n\n```\nawait router.callRouteByURL('/articles/abc123/upvote');\n```\n\nSee the [\"Bringing Some Routes\"](https://layrjs.com/docs/v1/introduction/routing) guide for a comprehensive example using the `Routable()` mixin.\n\n### RoutableComponent <badge type=\"primary\">class</badge> {#routable-component-class}\n\n*Inherits from [`Component`](https://layrjs.com/docs/v1/reference/component).*\n\nA `RoutableComponent` class is constructed by calling the `Routable()` mixin ([see above](https://layrjs.com/docs/v1/reference/routable#routable-mixin)).\n\n#### Component Methods\n\nSee the methods that are inherited from the [`Component`](https://layrjs.com/docs/v1/reference/component#creation) class.\n\n#### Router Registration\n\n##### `getRouter()` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#get-router-dual-method}\n\nReturns the router in which the routable component is registered. If the routable component is not registered in a router, an error is thrown.\n\n**Returns:**\n\nA [`Router`](https://layrjs.com/docs/v1/reference/router) instance.\n\n**Example:**\n\n```\nArticle.getRouter(); // => router\n```\n\n##### `hasRouter()` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#has-router-dual-method}\n\nReturns whether the routable component is registered in a router.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nArticle.hasRouter(); // => true\n```\n\n#### Routes\n\n##### `getRoute(name)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#get-route-dual-method}\n\nGets a route. If there is no route with the specified name, an error is thrown.\n\n**Parameters:**\n\n* `name`: The name of the route to get.\n\n**Returns:**\n\nA [Route](https://layrjs.com/docs/v1/reference/route) instance.\n\n**Example:**\n\n```\nArticle.getRoute('upvote'); => upvote() route\n```\n\n##### `hasRoute(name)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#has-route-dual-method}\n\nReturns whether the routable component has a route with the specified name.\n\n**Parameters:**\n\n* `name`: The name of the route to check.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nArticle.hasRoute('upvote'); // => true\n```\n\n##### `setRoute(name, pattern, [options])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#set-route-dual-method}\n\nSets a route in the storable component.\n\nTypically, instead of using this method, you would rather use the [`@route()`](https://layrjs.com/docs/v1/reference/routable#route-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the route.\n* `pattern`: A string specifying the [URL pattern](https://layrjs.com/docs/v1/reference/route#url-pattern-type) associated with the route.\n* `options`: An object specifying the options to pass to the `Route`'s [constructor](https://layrjs.com/docs/v1/reference/route#constructor) when the route is created.\n\n**Returns:**\n\nThe [Route](https://layrjs.com/docs/v1/reference/route) instance that was created.\n\n**Example:**\n\n```\nArticle.setRoute('upvote', '/articles/:id/upvote');\n```\n\n##### `callRoute(name, [params])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#call-route-dual-method}\n\nCalls the method associated to a route that has the specified name. If there is no route with the specified name, an error is thrown.\n\n**Parameters:**\n\n* `name`: The name of the route for which the associated method should be called.\n* `params`: The parameters to pass when the method is called.\n\n**Returns:**\n\nThe result of the called method.\n\n**Example:**\n\n```\nawait Article.callRoute('upvote', {id: 'abc123'});\n\n// Which is the equivalent of calling the `upvote()` method directly:\nawait Article.upvote({id: 'abc123'});\n```\n\n##### `findRouteByURL(url)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#find-route-by-url-dual-method}\n\nFinds the first route that matches the specified URL.\n\nIf no route matches the specified URL, returns `undefined`.\n\n**Parameters:**\n\n* `url`: A string or a [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) object.\n\n**Returns:**\n\nAn object of the shape `{route, params}` (or `undefined` if no route was found) where `route` is the [route](https://layrjs.com/docs/v1/reference/route) that was found, and `params` is a plain object representing the parameters that are included in the specified URL.\n\n**Example:**\n\n```\nconst {route, params} = Article.findRouteByURL('/articles/abc123/upvote');\n\nroute; // => upvote() route\nparams; // => {id: 'abc123'}\n```\n\n##### `callRouteByURL(url)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#call-route-by-url-dual-method}\n\nCalls the method associated to the first route that matches the specified URL.\n\nIf no route matches the specified URL, an error is thrown.\n\nWhen a route is found, the associated method is called with the parameters that are included in the specified URL.\n\n**Parameters:**\n\n* `url`: A string or a [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) object.\n\n**Returns:**\n\nThe result of the method associated to the route that was found.\n\n**Example:**\n\n```\nawait Article.callRouteByURL('/articles/abc123/upvote');\n\n// Which is the equivalent of calling the `upvote()` method directly:\nawait Article.upvote({id: 'abc123'});\n```\n\n#### Observability\n\nSee the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v1/reference/observable#observable-class) class.\n\n#### Decorators\n\n##### `@route(pattern, [options])` <badge type=\"tertiary\">decorator</badge> {#route-decorator}\n\nDefines a [route](https://layrjs.com/docs/v1/reference/route) for a static or instance method in a [routable component](https://layrjs.com/docs/v1/reference/routable#routable-component-class).\n\n**Parameters:**\n\n* `pattern`: The canonical [URL pattern](https://layrjs.com/docs/v1/reference/route#url-pattern-type) of the route.\n* `options`: An object specifying the options to pass to the `Route`'s [constructor](https://layrjs.com/docs/v1/reference/route#constructor) when the route is created.\n\n**Shortcut functions:**\n\nIn addition to defining a route, the decorator adds some shortcut functions to the decorated method so that you can interact with the route more easily.\n\nFor example, if you define a `route` for a `Home()` method you automatically get the following functions:\n\n- `Home.matchURL(url)` is the equivalent of [`route.matchURL(url)`](https://layrjs.com/docs/v1/reference/route#match-url-instance-method).\n- `Home.generateURL(params, options)` is the equivalent of [`route.generateURL(params, options)`](https://layrjs.com/docs/v1/reference/route#generate-url-instance-method).\n\nIf the defined `route` is controlled by a [`router`](https://layrjs.com/docs/v1/reference/router), you also get the following shortcut functions:\n\n- `Home.navigate(params, options)` is the equivalent of [`router.navigate(url, options)`](https://layrjs.com/docs/v1/reference/router#navigate-instance-method) where `url` is generated by calling [`route.generateURL(params, options)`](https://layrjs.com/docs/v1/reference/route#generate-url-instance-method).\n- `Home.redirect(params, options)` is the equivalent of [`router.redirect(url, options)`](https://layrjs.com/docs/v1/reference/router#redirect-instance-method) where `url` is generated by calling [`route.generateURL(params, options)`](https://layrjs.com/docs/v1/reference/route#generate-url-instance-method).\n- `Home.reload(params, options)` is the equivalent of [`router.reload(url)`](https://layrjs.com/docs/v1/reference/router#reload-instance-method) where `url` is generated by calling [`route.generateURL(params, options)`](https://layrjs.com/docs/v1/reference/route#generate-url-instance-method).\n- `Home.isActive(params)` returns a boolean indicating whether the `route`'s URL (generated by calling [`route.generateURL(params)`](https://layrjs.com/docs/v1/reference/route#generate-url-instance-method)) matches the current `router`'s URL.\n\nLastly, if the defined `route` is controlled by a [`router`](https://layrjs.com/docs/v1/reference/router) that is created by using the [`useBrowserRouter()`](https://layrjs.com/docs/v1/reference/react-integration#use-browser-router-react-hook) React hook, you also get the following shortcut function:\n\n- `Home.Link({params, hash, ...props})` is the equivalent of [`router.Link({to, ...props})`](https://layrjs.com/docs/v1/reference/browser-router#link-instance-method) where `to` is generated by calling [`route.generateURL(params, {hash})`](https://layrjs.com/docs/v1/reference/route#generate-url-instance-method).\n\n**Example:**\n\nSee an example of use in the [`BrowserRouter`](https://layrjs.com/docs/v1/reference/browser-router) class.\n#### Utilities\n\n##### `isRoutableClass(value)` <badge type=\"tertiary-outline\">function</badge> {#is-routable-class-function}\n\nReturns whether the specified value is a routable component class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isRoutableInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-routable-instance-function}\n\nReturns whether the specified value is a routable component class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isRoutableClassOrInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-routable-class-or-instance-function}\n\nReturns whether the specified value is a routable component class or instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `assertIsRoutableClass(value)` <badge type=\"tertiary-outline\">function</badge> {#assert-is-routable-class-function}\n\nThrows an error if the specified value is not a routable component class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n##### `assertIsRoutableInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#assert-is-routable-instance-function}\n\nThrows an error if the specified value is not a routable component instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n##### `assertIsRoutableClassOrInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#assert-is-routable-class-or-instance-function}\n\nThrows an error if the specified value is not a routable component class or instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/route-1rS9AS4eda7qe7lWTU4WKF.immutable.md",
    "content": "### Route <badge type=\"primary\">class</badge> {#route-class}\n\nRepresents a route in a [routable component](https://layrjs.com/docs/v1/reference/routable#routable-component-class).\n\nA route is composed of:\n\n- A name matching a class method of the [routable component](https://layrjs.com/docs/v1/reference/routable#routable-component-class) that contains the route.\n- The canonical [URL pattern](https://layrjs.com/docs/v1/reference/route#url-pattern-type) of the route.\n- Some [URL pattern](https://layrjs.com/docs/v1/reference/route#url-pattern-type) aliases.\n\n#### Usage\n\nTypically, you create a `Route` and associate it to a routable component by using the [`@route()`](https://layrjs.com/docs/v1/reference/routable#route-decorator) decorator.\n\nSee an example of use in the [`Routable()`](https://layrjs.com/docs/v1/reference/routable#usage) mixin.\n\n#### Creation\n\n##### `new Route(name, pattern, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates an instance of [`Route`](https://layrjs.com/docs/v1/reference/route). Typically, instead of using this constructor, you would rather use the [`@route()`](https://layrjs.com/docs/v1/reference/routable#route-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the route.\n* `pattern`: The canonical [URL pattern](https://layrjs.com/docs/v1/reference/route#url-pattern-type) of the route.\n* `options`:\n  * `aliases`: An array of alternate [URL patterns](https://layrjs.com/docs/v1/reference/route#url-pattern-type).\n\n**Returns:**\n\nThe [`Route`](https://layrjs.com/docs/v1/reference/route) instance that was created.\n\n**Example:**\n\n```\nconst route = new Route('Home', '/', {aliases: ['/home']});\n```\n\n#### Basic Methods\n\n##### `getName()` <badge type=\"secondary-outline\">instance method</badge> {#get-name-instance-method}\n\nReturns the name of the route.\n\n**Returns:**\n\nA string.\n\n**Example:**\n\n```\nconst route = new Route('Home', '/');\n\nroute.getName(); // => 'Home'\n```\n\n##### `getPattern()` <badge type=\"secondary-outline\">instance method</badge> {#get-pattern-instance-method}\n\nReturns the canonical URL pattern of the route.\n\n**Returns:**\n\nAn [URL pattern](https://layrjs.com/docs/v1/reference/route#url-pattern-type) string.\n\n**Example:**\n\n```\nconst route = new Route('Viewer', '/movies/:slug\\\\?:showDetails');\n\nroute.getPattern(); // => '/movies/:slug\\\\?:showDetails'\n```\n\n##### `getAliases()` <badge type=\"secondary-outline\">instance method</badge> {#get-aliases-instance-method}\n\nReturns the alternate URL patterns of the route.\n\n**Returns:**\n\nAn array of [URL pattern](https://layrjs.com/docs/v1/reference/route#url-pattern-type) strings.\n\n**Example:**\n\n```\nconst route = new Route('Home', '/', {aliases: ['/home']});\n\nroute.getAliases(); // => ['/home']\n```\n\n#### URL Matching and Generation\n\n##### `matchURL(url)` <badge type=\"secondary-outline\">instance method</badge> {#match-url-instance-method}\n\nChecks if the route matches the specified URL.\n\n**Parameters:**\n\n* `url`: A string or a [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) object.\n\n**Returns:**\n\nIf the route matches the specified URL, a plain object representing the parameters that are included in the URL (or an empty object if there is no parameters) is returned. Otherwise, `undefined` is returned.\n\n**Example:**\n\n```\nconst route = new Route('Viewer', '/movies/:slug\\\\?:showDetails');\n\nroute.matchURL('/movies/abc123'); // => {slug: 'abc123'}\n\nroute.matchURL('/movies/abc123?showDetails=1'); // => {slug: 'abc123', showDetails: '1'}\n\nroute.matchURL('/films'); // => undefined\n```\n\n##### `generateURL([params], [options])` <badge type=\"secondary-outline\">instance method</badge> {#generate-url-instance-method}\n\nGenerates an URL for the route.\n\n**Parameters:**\n\n* `params`: An optional object representing the parameters to include in the generated URL.\n* `options`:\n  * `hash`: A string representing an hash (i.e., a [fragment identifier](https://en.wikipedia.org/wiki/URI_fragment)) to include in the generated URL.\n\n**Returns:**\n\nA string.\n\n**Example:**\n\n```\nconst route = new Route('Viewer', '/movies/:slug\\\\?:showDetails');\n\nroute.generateURL({slug: 'abc123'}); // => '/movies/abc123'\n\nroute.generateURL({slug: 'abc123', showDetails: '1'}); // => '/movies/abc123?showDetails=1'\n\nroute.generateURL({}); // => Error (the slug parameter is mandatory)\n```\n\n#### Types\n\n##### `URLPattern` <badge type=\"primary-outline\">type</badge> {#url-pattern-type}\n\nA string representing the canonical URL pattern (or an alternate URL pattern) of a route.\n\nAn URL pattern is composed of a *path pattern* and an optional *query pattern* that are separated by an escaped question mark (`\\\\?`).\n\nA *path pattern* represents the path part of an URL and it can include some parameters by prefixing the name of each parameter with a colon sign (`:`). The [`path-to-regexp`](https://github.com/pillarjs/path-to-regexp) package is used under the hood to handle the path patterns, so any path pattern that is supported by `path-to-regexp` is supported by Layr as well.\n\nA *query pattern* represents the query part of an URL and it is composed of a list of parameters separated by an ampersand sign (`&`). Just like a path parameter, a query parameter is represented by a name prefixed with a colon sign (`:`). When an URL is matched against an URL pattern with the [`matchURL()`](https://layrjs.com/docs/v1/reference/route#match-url-instance-method) method, the [`qs`](https://github.com/ljharb/qs) package is used under the hood to parse the query part of the URL.\n\n**Examples:**\n\n- `'/'`: Root URL pattern.\n- `'/movies'`: URL pattern without parameters.\n- `'/movies/:id'`: URL pattern with one path parameter (`id`).\n- `'/movies/:movieId/actors/:actorId'`: URL pattern with two path parameter (`movieId` and `actorId`).\n- `'/movies\\\\?:sortBy'`: URL pattern with one query parameter (`sortBy`).\n- `'/movies\\\\?:sortBy&:offset'`: URL pattern with two query parameters (`sortBy` and `offset`).\n- `'/movies/:id\\\\?:showDetails'`: URL pattern with one path parameter (`id`) and one query parameter (`showDetails`).\n- `'/movies/:genre?'`: URL pattern with an [optional](https://github.com/pillarjs/path-to-regexp#optional) path parameter (`genre`).\n- `'/:slugs*'`: URL pattern with [zero or more](https://github.com/pillarjs/path-to-regexp#zero-or-more) path parameters (`slugs`).\n- `'/:slugs+'`: URL pattern with [one or more](https://github.com/pillarjs/path-to-regexp#one-or-more) path parameters (`slugs`).\n- `'/movies/:id(\\\\d+)'`: URL pattern with one path parameter (`id`) restricted to digits.\n\n#### Utilities\n\n##### `isRouteClass(value)` <badge type=\"tertiary-outline\">function</badge> {#is-route-class-function}\n\nReturns whether the specified value is a [`Route`](https://layrjs.com/docs/v1/reference/route) class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isRouteInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-route-instance-function}\n\nReturns whether the specified value is a [`Route`](https://layrjs.com/docs/v1/reference/route) instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/router-5zhzJL9MYM1yaHY6CXlkso.immutable.md",
    "content": "### Router <badge type=\"primary\">class</badge> {#router-class}\n\n*Inherits from [`Observable`](https://layrjs.com/docs/v1/reference/observable#observable-class).*\n\nAn abstract class from which classes such as [`BrowserRouter`](https://layrjs.com/docs/v1/reference/browser-router) or [`MemoryRouter`](https://layrjs.com/docs/v1/reference/memory-router) are constructed. Unless you build a custom router, you probably won't have to use this class directly.\n\n#### Component Registration\n\n##### `registerRootComponent(rootComponent)` <badge type=\"secondary-outline\">instance method</badge> {#register-root-component-instance-method}\n\nRegisters all the [routable components](https://layrjs.com/docs/v1/reference/routable#routable-component-class) that are provided (directly or recursively) by the specified root component.\n\n**Parameters:**\n\n* `rootComponent`: A [`Component`](https://layrjs.com/docs/v1/reference/component) class.\n\n**Example:**\n\n```\nimport {Component} from '@layr/component';\nimport {Routable} from '@layr/routable';\nimport {BrowserRouter} from '@layr/browser-router';\n\nclass User extends Routable(Component) {\n  // ...\n}\n\nclass Movie extends Routable(Component) {\n  // ...\n}\n\nclass Frontend extends Component {\n  @provide() static User = User;\n  @provide() static Movie = Movie;\n}\n\nconst router = new BrowserRouter();\n\nrouter.registerRootComponent(Frontend); // User and Movie will be registered\n```\n\n##### `getRootComponents()` <badge type=\"secondary-outline\">instance method</badge> {#get-root-components-instance-method}\n\nGets all the root components that are registered into the router.\n\n**Returns:**\n\nAn iterator of [`Component`](https://layrjs.com/docs/v1/reference/component) classes.\n\n##### `getRoutable(name)` <badge type=\"secondary-outline\">instance method</badge> {#get-routable-instance-method}\n\nGets a [routable component](https://layrjs.com/docs/v1/reference/routable#routable-component-class) that is registered into the router. An error is thrown if there is no routable component with the specified name.\n\n**Parameters:**\n\n* `name`: The name of the routable component to get.\n\n**Returns:**\n\nA [`RoutableComponent`](https://layrjs.com/docs/v1/reference/routable#routable-component-class) class.\n\n**Example:**\n\n```\n// See the definition of `router` in the `registerRootComponent()` example\n\nrouter.getRoutable('Movie'); // => Movie class\nrouter.getRoutable('User'); // => User class\nrouter.getRoutable('Film'); // => Error\n```\n\n##### `hasRoutable(name)` <badge type=\"secondary-outline\">instance method</badge> {#has-routable-instance-method}\n\nReturns whether a [routable component](https://layrjs.com/docs/v1/reference/routable#routable-component-class) is registered into the router.\n\n**Parameters:**\n\n* `name`: The name of the routable component to check.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\n// See the definition of `router` in the `registerRootComponent()` example\n\nrouter.hasRoutable('Movie'); // => true\nrouter.hasRoutable('User'); // => true\nrouter.hasRoutable('Film'); // => false\n```\n\n##### `registerRoutable(routable)` <badge type=\"secondary-outline\">instance method</badge> {#register-routable-instance-method}\n\nRegisters a specific [routable component](https://layrjs.com/docs/v1/reference/routable#routable-component-class) into the router. Typically, instead of using this method, you would rather use the [`registerRootComponent()`](https://layrjs.com/docs/v1/reference/router#register-root-component-instance-method) method to register multiple routable components at once.\n\n**Parameters:**\n\n* `routable`: The [`RoutableComponent`](https://layrjs.com/docs/v1/reference/routable#routable-component-class) class to register.\n\n**Example:**\n\n```\nclass Movie extends Routable(Component) {\n  // ...\n}\n\nconst router = new BrowserRouter();\n\nrouter.registerRoutable(Movie);\n```\n\n##### `getRoutables()` <badge type=\"secondary-outline\">instance method</badge> {#get-routables-instance-method}\n\nGets all the [routable components](https://layrjs.com/docs/v1/reference/routable#routable-component-class) that are registered into the router.\n\n**Returns:**\n\nAn iterator of [`RoutableComponent`](https://layrjs.com/docs/v1/reference/routable#routable-component-class) classes.\n\n#### Routes\n\n##### `findRouteByURL(url)` <badge type=\"secondary-outline\">instance method</badge> {#find-route-by-url-instance-method}\n\nFinds the first route that matches the specified URL.\n\nIf no route matches the specified URL, returns `undefined`.\n\n**Parameters:**\n\n* `url`: A string or a [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) object.\n\n**Returns:**\n\nAn object of the shape `{routable, route, params}` (or `undefined` if no route was found) where `routable` is the [`RoutableComponent`](https://layrjs.com/docs/v1/reference/routable#routable-component-class) containing the route that was found, `route` is the [route](https://layrjs.com/docs/v1/reference/route) that was found, and `params` is a plain object representing the parameters that are included in the specified URL.\n\n**Example:**\n\n```\nclass Movie extends Routable(Component) {\n  // ...\n\n  @route('/movies/:slug') @view() static Viewer() {\n    // ...\n  }\n}\n\nconst router = new BrowserRouter();\nrouter.registerRoutable(Movie);\n\nconst {routable, route, params} = router.findRouteByURL('/movies/inception');\n\nroutable; // => Movie class\nroute; // => Viewer() route\nparams; // => {slug: 'inception'}\n```\n\n##### `getParamsFromURL(url)` <badge type=\"secondary-outline\">instance method</badge> {#get-params-from-url-instance-method}\n\nReturns the parameters that are included in the specified URL.\n\nIf no route matches the specified URL, throws an error.\n\n**Parameters:**\n\n* `url`: A string or a [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) object.\n\n**Returns:**\n\nA plain object representing the parameters that are included in the specified URL.\n\n**Example:**\n\n```\n// See the definition of `router` in the `findRouteByURL()` example\n\nrouter.getParamsFromURL('/movies/inception'); // => {slug: 'inception'}\n```\n\n##### `callRouteByURL(url, [options])` <badge type=\"secondary-outline\">instance method</badge> {#call-route-by-url-instance-method}\n\nCalls the method associated to the first route that matches the specified URL.\n\nIf no route matches the specified URL, calls the specified fallback or throws an error if no fallback is specified.\n\nWhen a route is found, the associated method is called with the parameters that are included in the specified URL.\n\n**Parameters:**\n\n* `url`: A string or a [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) object.\n* `options`:\n  * `fallback`: A function to call in case no route matches the specified URL (default: `undefined`).\n\n**Returns:**\n\nThe result of the method associated to the route that was found or the result of the specified fallback if no route was found.\n\n**Example:**\n\n```\n// See the definition of `router` in the `findRouteByURL()` example\n\nrouter.callRouteByURL('/movies/inception'); // => Some React elements\n\n// `Movie.Viewer()` was called as follows:\n// Movie.Viewer({slug: 'inception'});\n```\n\n#### Current Location\n\n##### `getCurrentURL()` <badge type=\"secondary-outline\">instance method</badge> {#get-current-url-instance-method}\n\nReturns the current URL of the router.\n\n**Returns:**\n\nA string.\n\n**Example:**\n\n```\n// See the definition of `router` in the `findRouteByURL()` example\n\nrouter.navigate('/movies/inception?showDetails=1#actors');\nrouter.getCurrentURL(); // => /movies/inception?showDetails=1#actors'\n```\n\n##### `getCurrentParams()` <badge type=\"secondary-outline\">instance method</badge> {#get-current-params-instance-method}\n\nReturns the parameters that are included in the current URL of the router.\n\n**Returns:**\n\nA plain object.\n\n**Example:**\n\n```\n// See the definition of `router` in the `findRouteByURL()` example\n\nrouter.navigate('/movies/inception?showDetails=1#actors');\nrouter.getCurrentParams(); // => {slug: 'inception'}\n```\n\n##### `getCurrentPath()` <badge type=\"secondary-outline\">instance method</badge> {#get-current-path-instance-method}\n\nReturns the path of the current URL.\n\n**Returns:**\n\nA string.\n\n**Example:**\n\n```\n// See the definition of `router` in the `findRouteByURL()` example\n\nrouter.navigate('/movies/inception?showDetails=1#actors');\nrouter.getCurrentPath(); // => '/movies/inception'\n```\n\n##### `getCurrentQuery()` <badge type=\"secondary-outline\">instance method</badge> {#get-current-query-instance-method}\n\nReturns an object representing the query of the current URL.\n\nThe [`qs`](https://github.com/ljharb/qs) package is used under the hood to parse the query.\n\n**Returns:**\n\nA plain object.\n\n**Example:**\n\n```\n// See the definition of `router` in the `findRouteByURL()` example\n\nrouter.navigate('/movies/inception?showDetails=1#actors');\nrouter.getCurrentQuery(); // => {showDetails: '1'}\n```\n\n##### `getCurrentHash()` <badge type=\"secondary-outline\">instance method</badge> {#get-current-hash-instance-method}\n\nReturns the hash (i.e., the [fragment identifier](https://en.wikipedia.org/wiki/URI_fragment)) contained in the current URL. If the current URL doesn't contain a hash, returns `undefined`.\n\n**Returns:**\n\nA string or `undefined`.\n\n**Example:**\n\n```\n// See the definition of `router` in the `findRouteByURL()` example\n\nrouter.navigate('/movies/inception?showDetails=1#actors');\nrouter.getCurrentHash(); // => 'actors'\n\nrouter.navigate('/movies/inception?showDetails=1#actors');\nrouter.getCurrentHash(); // => 'actors'\n\nrouter.navigate('/movies/inception?showDetails=1#');\nrouter.getCurrentHash(); // => undefined\n\nrouter.navigate('/movies/inception?showDetails=1');\nrouter.getCurrentHash(); // => undefined\n```\n\n##### `callCurrentRoute([options])` <badge type=\"secondary-outline\">instance method</badge> {#call-current-route-instance-method}\n\nCalls the method associated to the first route that matches the current URL.\n\nIf no route matches the current URL, calls the specified fallback or throws an error if no fallback is specified.\n\nWhen a route is found, the associated method is called with the parameters that are included in the current URL.\n\n**Parameters:**\n\n* `options`:\n  * `fallback`: A function to call in case no route matches the current URL (default: `undefined`).\n\n**Returns:**\n\nThe result of the method associated to the route that was found or the result of the specified fallback if no route was found.\n\n**Example:**\n\n```\n// See the definition of `router` in the `findRouteByURL()` example\n\nrouter.navigate('/movies/inception');\nrouter.callCurrentRoute(); // => Some React elements\n\n// `Movie.Viewer()` was called as follows:\n// Movie.Viewer({slug: 'inception'});\n```\n\n#### Navigation\n\n##### `navigate(url, [options])` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">possibly async</badge> {#navigate-instance-method}\n\nNavigates to a URL.\n\nThe specified URL is added to the router's history.\n\nThe observers of the router are automatically called.\n\nNote that instead of using this method, you can use the handy `navigate()` shortcut function that you get when you define a route with the [`@route()`](https://layrjs.com/docs/v1/reference/routable#route-decorator) decorator.\n\n**Parameters:**\n\n* `url`: A string or a [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) object.\n* `options`:\n  * `silent`: A boolean specifying whether the router's observers should *not* be called (default: `false`).\n  * `defer`: A boolean specifying whether the calling of the router's observers should be deferred to the next tick (default: `false`).\n\n**Example:**\n\n```\nrouter.navigate('/movies/inception');\n\n// Same as above, but in a more idiomatic way:\nMovie.Viewer.navigate({slug: 'inception});\n```\n\n##### `redirect(url, [options])` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">possibly async</badge> {#redirect-instance-method}\n\nRedirects to a URL.\n\nThe specified URL replaces the current entry of the router's history.\n\nThe observers of the router are automatically called.\n\nNote that instead of using this method, you can use the handy `redirect()` shortcut function that you get when you define a route with the [`@route()`](https://layrjs.com/docs/v1/reference/routable#route-decorator) decorator.\n\n**Parameters:**\n\n* `url`: A string or a [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) object.\n* `options`:\n  * `silent`: A boolean specifying whether the router's observers should *not* be called (default: `false`).\n  * `defer`: A boolean specifying whether the calling of the router's observers should be deferred to the next tick (default: `false`).\n\n**Example:**\n\n```\nrouter.redirect('/sign-in');\n\n// Same as above, but in a more idiomatic way:\nSession.SignIn.redirect();\n```\n\n##### `reload(url)` <badge type=\"secondary-outline\">instance method</badge> {#reload-instance-method}\n\nReloads the execution environment with the specified URL.\n\nNote that instead of using this method, you can use the handy `reload()` shortcut function that you get when you define a route with the [`@route()`](https://layrjs.com/docs/v1/reference/routable#route-decorator) decorator.\n\n**Parameters:**\n\n* `url`: A string or a [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) object.\n\n**Example:**\n\n```\nrouter.reload('/');\n\n// Same as above, but in a more idiomatic way:\nFrontend.Home.reload();\n```\n\n##### `go(delta, [options])` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">possibly async</badge> {#go-instance-method}\n\nMove forwards or backwards through the router's history.\n\nThe observers of the router are automatically called.\n\n**Parameters:**\n\n* `delta`: A number representing the position in the router's history to which you want to move, relative to the current entry. A negative value moves backwards, a positive value moves forwards.\n* `options`:\n  * `silent`: A boolean specifying whether the router's observers should *not* be called (default: `false`).\n  * `defer`: A boolean specifying whether the calling of the router's observers should be deferred to the next tick (default: `false`).\n\n**Example:**\n\n```\nrouter.go(-2); // Move backwards by two entries of the router's history\n\nrouter.go(-1); // Equivalent of calling `router.goBack()`\n\nrouter.go(1); // Equivalent of calling `router.goForward()`\n\nrouter.go(2); // Move forward two entries of the router's history\n```\n\n##### `goBack([options])` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">possibly async</badge> {#go-back-instance-method}\n\nGo back to the previous entry in the router's history.\n\nThis method is the equivalent of calling `router.go(-1)`.\n\nThe observers of the router are automatically called.\n\n**Parameters:**\n\n* `options`:\n  * `silent`: A boolean specifying whether the router's observers should *not* be called (default: `false`).\n  * `defer`: A boolean specifying whether the calling of the router's observers should be deferred to the next tick (default: `false`).\n\n##### `goForward([options])` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">possibly async</badge> {#go-forward-instance-method}\n\nGo forward to the next entry in the router's history.\n\nThis method is the equivalent of calling `router.go(1)`.\n\nThe observers of the router are automatically called.\n\n**Parameters:**\n\n* `options`:\n  * `silent`: A boolean specifying whether the router's observers should *not* be called (default: `false`).\n  * `defer`: A boolean specifying whether the calling of the router's observers should be deferred to the next tick (default: `false`).\n\n##### `getHistoryLength()` <badge type=\"secondary-outline\">instance method</badge> {#get-history-length-instance-method}\n\nReturns the number of entries in the router's history.\n\n#### Observability\n\nSee the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v1/reference/observable#observable-class) class.\n\n#### Utilities\n\n##### `isRouterClass(value)` <badge type=\"tertiary-outline\">function</badge> {#is-router-class-function}\n\nReturns whether the specified value is a router class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isRouterInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-router-instance-function}\n\nReturns whether the specified value is a router instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/secondary-identifier-attribute-7BqK9EmnCMCHIAroYiQDth.immutable.md",
    "content": "### SecondaryIdentifierAttribute <badge type=\"primary\">class</badge> {#secondary-identifier-attribute-class}\n\n*Inherits from [`IdentifierAttribute`](https://layrjs.com/docs/v1/reference/identifier-attribute).*\n\nA `SecondaryIdentifierAttribute` is a special kind of attribute that uniquely identify a [Component](https://layrjs.com/docs/v1/reference/component) instance.\n\nContrary to a [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/primary-identifier-attribute), you can define more than one `SecondaryIdentifierAttribute` in a `Component`.\n\nAnother difference with a `PrimaryIdentifierAttribute` is that a `SecondaryIdentifierAttribute` value is mutable (i.e., it can change over time).\n\n#### Usage\n\nTypically, you create a `SecondaryIdentifierAttribute` and associate it to a component prototype using the [`@secondaryIdentifier()`](https://layrjs.com/docs/v1/reference/component#secondary-identifier-decorator) decorator.\n\nA common use case is a `User` component with an immutable primary identifier and a secondary identifier for the email address that can change over time:\n\n```\n// JS\n\nimport {Component, primaryIdentifier, secondaryIdentifier} from '@layr/component';\n\nclass User extends Component {\n  @primaryIdentifier() id;\n  @secondaryIdentifier() email;\n}\n```\n\n```\n// TS\n\nimport {Component, primaryIdentifier, secondaryIdentifier} from '@layr/component';\n\nclass User extends Component {\n  @primaryIdentifier() id!: string;\n  @secondaryIdentifier() email!: string;\n}\n```\n\nTo create a `User` instance, you would do something like:\n\n```\nconst user = new User({email: 'someone@domain.tld'});\n\nuser.id; // => 'ck41vli1z00013h5xx1esffyn'\nuser.email; // => 'someone@domain.tld'\n```\n\nNote that the primary identifier (`id`) was auto-generated, but we had to provide a value for the secondary identifier (`email`) because secondary identifiers cannot be `undefined` and they are not commonly auto-generated.\n\nLike previously mentioned, contrary to a primary identifier, the value of a secondary identifer can be changed:\n\n```\nuser.email = 'someone-else@domain.tld'; // Okay\nuser.id = 'ck2zrb1xs00013g5to1uimigb'; // Error\n```\n\n`SecondaryIdentifierAttribute` values are usually of type `'string'` (the default), but you can also have values of type `'number'`:\n\n```\n// JS\n\nimport {Component, primaryIdentifier, secondaryIdentifier} from '@layr/component';\n\nclass User extends Component {\n  @primaryIdentifier() id;\n  @secondaryIdentifier('number') reference;\n}\n```\n\n```\n// TS\n\nimport {Component, primaryIdentifier, secondaryIdentifier} from '@layr/component';\n\nclass User extends Component {\n  @primaryIdentifier() id!: string;\n  @secondaryIdentifier('number') reference!: number;\n}\n```\n\n#### Creation\n\n##### `new SecondaryIdentifierAttribute(name, parent, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates an instance of [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/secondary-identifier-attribute). Typically, instead of using this constructor, you would rather use the [`@secondaryIdentifier()`](https://layrjs.com/docs/v1/reference/component#secondary-identifier-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the attribute.\n* `parent`: The component prototype that owns the attribute.\n* `options`:\n  * `valueType`: A string specifying the type of values the attribute can store. It can be either `'string'` or `'number'` (default: `'string'`).\n  * `default`: A function returning the default value of the attribute.\n  * `validators`: An array of [validators](https://layrjs.com/docs/v1/reference/validator) for the value of the attribute.\n  * `exposure`: A [`PropertyExposure`](https://layrjs.com/docs/v1/reference/property#property-exposure-type) object specifying how the attribute should be exposed to remote access.\n\n**Returns:**\n\nThe [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/secondary-identifier-attribute) instance that was created.\n\n**Example:**\n\n```\nimport {Component, SecondaryIdentifierAttribute} from '@layr/component';\n\nclass User extends Component {}\n\nconst email = new SecondaryIdentifierAttribute('email', User.prototype);\n\nemail.getName(); // => 'email'\nemail.getParent(); // => User.prototype\nemail.getValueType().toString(); // => 'string'\n```\n\n#### Property Methods\n\nSee the methods that are inherited from the [`Property`](https://layrjs.com/docs/v1/reference/property#basic-methods) class.\n\n#### Attribute Methods\n\nSee the methods that are inherited from the [`Attribute`](https://layrjs.com/docs/v1/reference/attribute#value-type) class.\n\n#### Observability\n\nSee the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v1/reference/observable#observable-class) class.\n\n#### Utilities\n\n##### `isSecondaryIdentifierAttributeClass(value)` <badge type=\"tertiary-outline\">function</badge> {#is-secondary-identifier-attribute-class-function}\n\nReturns whether the specified value is a `SecondaryIdentifierAttribute` class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isSecondaryIdentifierAttributeInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-secondary-identifier-attribute-instance-function}\n\nReturns whether the specified value is a `SecondaryIdentifierAttribute` instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/storable-2maAYPX5kWufBfVolYVgdc.immutable.md",
    "content": "### Storable() <badge type=\"primary\">mixin</badge> {#storable-mixin}\n\nExtends a [`Component`](https://layrjs.com/docs/v1/reference/component) class with some storage capabilities.\n\n#### Usage\n\nThe `Storable()` mixin can be used both in the backend and the frontend.\n\n##### Backend Usage\n\nCall `Storable()` with a [`Component`](https://layrjs.com/docs/v1/reference/component) class to construct a [`StorableComponent`](https://layrjs.com/docs/v1/reference/storable#storable-component-class) class that you can extend with your data model and business logic. Then, register this class into a store such as [`MongoDBStore`](https://layrjs.com/docs/v1/reference/mongodb-store) by using the [`registerStorable()`](https://layrjs.com/docs/v1/reference/store#register-storable-instance-method) method (or [`registerRootComponent()`](https://layrjs.com/docs/v1/reference/store#register-root-component-instance-method) to register several components at once).\n\n**Example:**\n\n```js\n// JS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\nimport {MongoDBStore} from '@layr/mongodb-store';\n\nexport class Movie extends Storable(Component) {\n  @primaryIdentifier() id;\n\n  @attribute() title = '';\n}\n\nconst store = new MongoDBStore('mongodb://user:pass@host:port/db');\n\nstore.registerStorable(Movie);\n```\n\n```ts\n// TS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\nimport {MongoDBStore} from '@layr/mongodb-store';\n\nexport class Movie extends Storable(Component) {\n  @primaryIdentifier() id!: string;\n\n  @attribute() title = '';\n}\n\nconst store = new MongoDBStore('mongodb://user:pass@host:port/db');\n\nstore.registerStorable(Movie);\n```\n\nOnce you have a storable component registered into a store, you can use any method provided by the `Storable()` mixin to interact with the database:\n\n```\nconst movie = new Movie({id: 'abc123', title: 'Inception'});\n\n// Save the movie to the database\nawait movie.save();\n\n// Retrieve the movie from the database\nawait Movie.get('abc123'); // => movie\n```\n\n##### Frontend Usage\n\nTypically, you construct a storable component in the frontend by \"inheriting\" a storable component exposed by the backend. To accomplish that, you create a [`ComponentHTTPClient`](https://layrjs.com/docs/v1/reference/component-http-client), and then call the [`getComponent()`](https://layrjs.com/docs/v1/reference/component-http-client#get-component-instance-method) method to construct your frontend component.\n\n**Example:**\n\n```\nimport {ComponentHTTPClient} from '@layr/component-http-client';\nimport {Storable} from '@layr/storable';\n\n(async () => {\n  const client = new ComponentHTTPClient('https://...', {\n    mixins: [Storable]\n  });\n\n  const Movie = await client.getComponent();\n})();\n```\n\n> Note that you have to pass the `Storable` mixin when you create a `ComponentHTTPClient` that is consuming a storable component.\n\nOnce you have a storable component in the frontend, you can use any method that is exposed by the backend. For example, if the `Movie`'s [`save()`](https://layrjs.com/docs/v1/reference/storable#save-instance-method) method is exposed by the backend, you can call it from the frontend to add a new movie into the database:\n\n```\nconst movie = new Movie({title: 'Inception 2'});\n\nawait movie.save();\n```\n\nSee the [\"Storing Data\"](https://layrjs.com/docs/v1/introduction/data-storage) guide for a comprehensive example using the `Storable()` mixin.\n\n### StorableComponent <badge type=\"primary\">class</badge> {#storable-component-class}\n\n*Inherits from [`Component`](https://layrjs.com/docs/v1/reference/component).*\n\nA `StorableComponent` class is constructed by calling the `Storable()` mixin ([see above](https://layrjs.com/docs/v1/reference/storable#storable-mixin)).\n\n#### Component Methods\n\nSee the methods that are inherited from the [`Component`](https://layrjs.com/docs/v1/reference/component#creation) class.\n\n#### Store Registration\n\n##### `getStore()` <badge type=\"secondary\">class method</badge> {#get-store-class-method}\n\nReturns the store in which the storable component is registered. If the storable component is not registered in a store, an error is thrown.\n\n**Returns:**\n\nA [`Store`](https://layrjs.com/docs/v1/reference/store) instance.\n\n**Example:**\n\n```\nMovie.getStore(); // => store\n```\n\n##### `hasStore()` <badge type=\"secondary\">class method</badge> {#has-store-class-method}\n\nReturns whether the storable component is registered in a store.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nMovie.hasStore(); // => true\n```\n\n#### Storage Operations\n\n##### `get(identifier, [attributeSelector], [options])` <badge type=\"secondary\">class method</badge> <badge type=\"outline\">async</badge> {#get-class-method}\n\nRetrieves a storable component instance (and possibly, some of its referenced components) from the store.\n\n> This method uses the [`load()`](https://layrjs.com/docs/v1/reference/storable#load-instance-method) method under the hood to load the component's attributes. So if you want to expose the [`get()`](https://layrjs.com/docs/v1/reference/storable#get-class-method) method to the frontend, you will typically have to expose the [`load()`](https://layrjs.com/docs/v1/reference/storable#load-instance-method) method as well.\n\n**Parameters:**\n\n* `identifier`: A plain object specifying the identifier of the component you want to retrieve. The shape of the object should be `{[identifierName]: identifierValue}`. Alternatively, you can specify a string or a number representing the value of a [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/primary-identifier-attribute).\n* `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v1/reference/attribute-selector) specifying the attributes to be loaded (default: `true`, which means that all the attributes will be loaded).\n* `options`:\n  * `reload`: A boolean specifying whether a component that has already been loaded should be loaded again from the store (default: `false`). Most of the time you will leave this option off to take advantage of the cache.\n  * `throwIfMissing`: A boolean specifying whether an error should be thrown if there is no component matching the specified `identifier` in the store (default: `true`).\n\n**Returns:**\n\nA [`StorableComponent`](https://layrjs.com/docs/v1/reference/storable#storable-component-class) instance.\n\n**Example:**\n\n```\n// Fully retrieve a movie by its primary identifier\nawait Movie.get({id: 'abc123'});\n\n// Same as above, but in a short manner\nawait Movie.get('abc123');\n\n// Fully retrieve a movie by its secondary identifier\nawait Movie.get({slug: 'inception'});\n\n// Partially retrieve a movie by its primary identifier\nawait Movie.get({id: 'abc123'}, {title: true, rating: true});\n\n// Partially retrieve a movie, and fully retrieve its referenced director component\nawait Movie.get({id: 'abc123'}, {title: true, director: true});\n\n// Partially retrieve a movie, and partially retrieve its referenced director component\nawait Movie.get({id: 'abc123'}, {title: true, director: {fullName: true}});\n```\n\n##### `has(identifier, [options])` <badge type=\"secondary\">class method</badge> <badge type=\"outline\">async</badge> {#has-class-method}\n\nReturns whether a storable component instance exists in the store.\n\n**Parameters:**\n\n* `identifier`: A plain object specifying the identifier of the component you want to search. The shape of the object should be `{[identifierName]: identifierValue}`. Alternatively, you can specify a string or a number representing the value of a [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/primary-identifier-attribute).\n* `options`:\n  * `reload`: A boolean specifying whether a component that has already been loaded should be searched again from the store (default: `false`). Most of the time you will leave this option off to take advantage of the cache.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\n// Check if there is a movie with a certain primary identifier\nawait Movie.has({id: 'abc123'}); // => true\n\n// Same as above, but in a short manner\nawait Movie.has('abc123'); // => true\n\n// Check if there is a movie with a certain secondary identifier\nawait Movie.has({slug: 'inception'}); // => true\n```\n\n##### `load([attributeSelector], [options])` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#load-instance-method}\n\nLoads some attributes of the current storable component instance (and possibly, some of its referenced components) from the store.\n\n**Parameters:**\n\n* `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v1/reference/attribute-selector) specifying the attributes to be loaded (default: `true`, which means that all the attributes will be loaded).\n* `options`:\n  * `reload`: A boolean specifying whether a component that has already been loaded should be loaded again from the store (default: `false`). Most of the time you will leave this option off to take advantage of the cache.\n  * `throwIfMissing`: A boolean specifying whether an error should be thrown if there is no matching component in the store (default: `true`).\n\n**Returns:**\n\nThe current [`StorableComponent`](https://layrjs.com/docs/v1/reference/storable#storable-component-class) instance.\n\n**Example:**\n\n```\n// Retrieve a movie with the 'title' attribute only\nconst movie = await Movie.get('abc123', {title: true});\n\n// Load a few more movie's attributes\nawait movie.load({tags: true, rating: true});\n\n// Load some attributes of the movie's director\nawait movie.load({director: {fullName: true}});\n\n// Since the movie's rating has already been loaded,\n// it will not be loaded again from the store\nawait movie.load({rating: true});\n\n// Change the movie's rating\nmovie.rating = 8.5;\n\n// Since the movie's rating has been modified,\n// it will be loaded again from the store\nawait movie.load({rating: true});\n\n// Force reloading the movie's rating\nawait movie.load({rating: true}, {reload: true});\n```\n\n##### `save([attributeSelector], [options])` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#save-instance-method}\n\nSaves the current storable component instance to the store. If the component is new, it will be added to the store with all its attributes. Otherwise, only the attributes that have been modified will be saved to the store.\n\n**Parameters:**\n\n* `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v1/reference/attribute-selector) specifying the attributes to be saved (default: `true`, which means that all the modified attributes will be saved).\n* `options`:\n  * `throwIfMissing`: A boolean specifying whether an error should be thrown if the current component is not new and there is no existing component with the same identifier in the store (default: `true` if the component is not new).\n  * `throwIfExists`: A boolean specifying whether an error should be thrown if the current component is new and there is an existing component with the same identifier in the store (default: `true` if the component is new).\n\n**Returns:**\n\nThe current [`StorableComponent`](https://layrjs.com/docs/v1/reference/storable#storable-component-class) instance.\n\n**Example:**\n\n```\n// Retrieve a movie with a few attributes\nconst movie = await Movie.get('abc123', {title: true, rating: true});\n\n// Change the movie's rating\nmovie.rating = 8;\n\n// Save the new movie's rating to the store\nawait movie.save();\n\n// Since the movie's rating has not been changed since the previous save(),\n// it will not be saved again\nawait movie.save();\n```\n\n##### `delete([options])` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#delete-instance-method}\n\nDeletes the current storable component instance from the store.\n\n**Parameters:**\n\n* `options`:\n  * `throwIfMissing`: A boolean specifying whether an error should be thrown if there is no matching component in the store (default: `true`).\n\n**Returns:**\n\nThe current [`StorableComponent`](https://layrjs.com/docs/v1/reference/storable#storable-component-class) instance.\n\n**Example:**\n\n```\n// Retrieve a movie\nconst movie = await Movie.get('abc123');\n\n// Delete the movie\nawait movie.delete();\n```\n\n##### `find([query], [attributeSelector], [options])` <badge type=\"secondary\">class method</badge> <badge type=\"outline\">async</badge> {#find-class-method}\n\nFinds some storable component instances matching the specified query in the store, and load all or some of their attributes (and possibly, load some of their referenced components as well).\n\n> This method uses the [`load()`](https://layrjs.com/docs/v1/reference/storable#load-instance-method) method under the hood to load the components' attributes. So if you want to expose the [`find()`](https://layrjs.com/docs/v1/reference/storable#find-class-method) method to the frontend, you will typically have to expose the [`load()`](https://layrjs.com/docs/v1/reference/storable#load-instance-method) method as well.\n\n**Parameters:**\n\n* `query`: A [`Query`](https://layrjs.com/docs/v1/reference/query) object specifying the criteria to be used when selecting the components from the store (default: `{}`, which means that any component can be selected).\n* `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v1/reference/attribute-selector) specifying the attributes to be loaded (default: `true`, which means that all the attributes will be loaded).\n* `options`:\n  * `sort`: A plain object specifying how the found components should be sorted (default: `undefined`). The shape of the object should be `{[name]: direction}` where `name` is the name of an attribute, and `direction` is the string `'asc'` or `'desc'` representing the sort direction (ascending or descending).\n  * `skip`: A number specifying how many components should be skipped from the found components (default: `0`).\n  * `limit`: A number specifying the maximum number of components that should be returned (default: `undefined`).\n  * `reload`: A boolean specifying whether a component that has already been loaded should be loaded again from the store (default: `false`). Most of the time you will leave this option off to take advantage of the cache.\n\n**Returns:**\n\nAn array of [`StorableComponent`](https://layrjs.com/docs/v1/reference/storable#storable-component-class) instances.\n\n**Example:**\n\n```\n// Find all the movies\nawait Movie.find();\n\n// Find the Japanese movies\nawait Movie.find({country: 'Japan'});\n\n// Find the Japanese drama movies\nawait Movie.find({country: 'Japan', genre: 'drama'});\n\n// Find the Tarantino's movies\nconst tarantino = await Director.get({slug: 'quentin-tarantino'});\nawait Movie.find({director: tarantino});\n\n// Find the movies released after 2010\nawait Movie.find({year: {$greaterThan: 2010}});\n\n// Find the top 30 movies\nawait Movie.find({}, true, {sort: {rating: 'desc'}, limit: 30});\n\n// Find the next top 30 movies\nawait Movie.find({}, true, {sort: {rating: 'desc'}, skip: 30, limit: 30});\n```\n\n##### `count([query])` <badge type=\"secondary\">class method</badge> <badge type=\"outline\">async</badge> {#count-class-method}\n\nCounts the number of storable component instances matching the specified query in the store.\n\n**Parameters:**\n\n* `query`: A [`Query`](https://layrjs.com/docs/v1/reference/query) object specifying the criteria to be used when selecting the components from the store (default: `{}`, which means that any component can be selected, and therefore the total number of components available in the store will be returned).\n\n**Returns:**\n\nA number.\n\n**Example:**\n\n```\n// Count the total number of movies\nawait Movie.count();\n\n// Count the number of Japanese movies\nawait Movie.count({country: 'Japan'});\n\n// Count the number of Japanese drama movies\nawait Movie.count({country: 'Japan', genre: 'drama'});\n\n// Count the number of Tarantino's movies\nconst tarantino = await Director.get({slug: 'quentin-tarantino'})\nawait Movie.count({director: tarantino});\n\n// Count the number of movies released after 2010\nawait Movie.count({year: {$greaterThan: 2010}});\n```\n\n#### isDeleted Mark\n\n##### `getIsDeletedMark()` <badge type=\"secondary-outline\">instance method</badge> {#get-is-deleted-mark-instance-method}\n\nReturns whether the component instance is marked as deleted or not.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nmovie.getIsDeletedMark(); // => false\nawait movie.delete();\nmovie.getIsDeletedMark(); // => true\n```\n\n##### `setIsDeletedMark(isDeleted)` <badge type=\"secondary-outline\">instance method</badge> {#set-is-deleted-mark-instance-method}\n\nSets whether the component instance is marked as deleted or not.\n\n**Parameters:**\n\n* `isDeleted`: A boolean specifying if the component instance should be marked as deleted or not.\n\n**Example:**\n\n```\nmovie.getIsDeletedMark(); // => false\nmovie.setIsDeletedMark(true);\nmovie.getIsDeletedMark(); // => true\n```\n\n#### Hooks\n\n##### `beforeLoad(attributeSelector)` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#before-load-instance-method}\n\nA method that you can override to execute some custom logic just before the current storable component instance is loaded from the store.\n\nThis method is automatically called when the [`load()`](https://layrjs.com/docs/v1/reference/storable#load-instance-method), [`get()`](https://layrjs.com/docs/v1/reference/storable#get-class-method), or [`find()`](https://layrjs.com/docs/v1/reference/storable#find-class-method) method is called, and there are some attributes to load. If all the attributes have already been loaded by a previous operation, unless the `reload` option is used, this method is not called.\n\n**Parameters:**\n\n* `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v1/reference/attribute-selector) indicating the attributes that will be loaded.\n\n**Example:**\n\n```\n// JS\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  async beforeLoad(attributeSelector) {\n    // Don't forget to call the parent method\n    await super.beforeLoad(attributeSelector);\n\n    // Implement your custom logic here\n  }\n}\n```\n```\n// TS\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  async beforeLoad(attributeSelector: AttributeSelector) {\n    // Don't forget to call the parent method\n    await super.beforeLoad(attributeSelector);\n\n    // Implement your custom logic here\n  }\n}\n```\n\n##### `afterLoad(attributeSelector)` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#after-load-instance-method}\n\nA method that you can override to execute some custom logic just after the current storable component instance has been loaded from the store.\n\nThis method is automatically called when the [`load()`](https://layrjs.com/docs/v1/reference/storable#load-instance-method), [`get()`](https://layrjs.com/docs/v1/reference/storable#get-class-method), or [`find()`](https://layrjs.com/docs/v1/reference/storable#find-class-method) method is called, and there were some attributes to load. If all the attributes have already been loaded by a previous operation, unless the `reload` option is used, this method is not called.\n\n**Parameters:**\n\n* `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v1/reference/attribute-selector) indicating the attributes that were loaded.\n\n**Example:**\n\n```\n// JS\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  async afterLoad(attributeSelector) {\n    // Don't forget to call the parent method\n    await super.afterLoad(attributeSelector);\n\n    // Implement your custom logic here\n  }\n}\n```\n```\n// TS\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  async afterLoad(attributeSelector: AttributeSelector) {\n    // Don't forget to call the parent method\n    await super.afterLoad(attributeSelector);\n\n    // Implement your custom logic here\n  }\n}\n```\n\n##### `beforeSave(attributeSelector)` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#before-save-instance-method}\n\nA method that you can override to execute some custom logic just before the current storable component instance is saved to the store.\n\nThis method is automatically called when the [`save()`](https://layrjs.com/docs/v1/reference/storable#save-instance-method) method is called, and there are some modified attributes to save. If no attributes were modified, this method is not called.\n\n**Parameters:**\n\n* `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v1/reference/attribute-selector) indicating the attributes that will be saved.\n\n**Example:**\n\n```\n// JS\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  async beforeSave(attributeSelector) {\n    // Don't forget to call the parent method\n    await super.beforeSave(attributeSelector);\n\n    // Implement your custom logic here\n  }\n}\n```\n```\n// TS\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  async beforeSave(attributeSelector: AttributeSelector) {\n    // Don't forget to call the parent method\n    await super.beforeSave(attributeSelector);\n\n    // Implement your custom logic here\n  }\n}\n```\n\n##### `afterSave(attributeSelector)` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#after-save-instance-method}\n\nA method that you can override to execute some custom logic just after the current storable component instance has been saved to the store.\n\nThis method is automatically called when the [`save()`](https://layrjs.com/docs/v1/reference/storable#save-instance-method) method is called, and there were some modified attributes to save. If no attributes were modified, this method is not called.\n\n**Parameters:**\n\n* `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v1/reference/attribute-selector) indicating the attributes that were saved.\n\n**Example:**\n\n```\n// JS\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  async afterSave(attributeSelector) {\n    // Don't forget to call the parent method\n    await super.afterSave(attributeSelector);\n\n    // Implement your custom logic here\n  }\n}\n```\n```\n// TS\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  async afterSave(attributeSelector: AttributeSelector) {\n    // Don't forget to call the parent method\n    await super.afterSave(attributeSelector);\n\n    // Implement your custom logic here\n  }\n}\n```\n\n##### `beforeDelete(attributeSelector)` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#before-delete-instance-method}\n\nA method that you can override to execute some custom logic just before the current storable component instance is deleted from the store.\n\nThis method is automatically called when the [`delete()`](https://layrjs.com/docs/v1/reference/storable#delete-instance-method) method is called.\n\n**Parameters:**\n\n* `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v1/reference/attribute-selector) indicating the attributes that will be deleted.\n\n**Example:**\n\n```\n// JS\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  async beforeDelete(attributeSelector) {\n    // Don't forget to call the parent method\n    await super.beforeDelete(attributeSelector);\n\n    // Implement your custom logic here\n  }\n}\n```\n```\n// TS\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  async beforeDelete(attributeSelector: AttributeSelector) {\n    // Don't forget to call the parent method\n    await super.beforeDelete(attributeSelector);\n\n    // Implement your custom logic here\n  }\n}\n```\n\n##### `afterDelete(attributeSelector)` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#after-delete-instance-method}\n\nA method that you can override to execute some custom logic just after the current storable component instance has been deleted from the store.\n\nThis method is automatically called when the [`delete()`](https://layrjs.com/docs/v1/reference/storable#delete-instance-method) method is called.\n\n**Parameters:**\n\n* `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v1/reference/attribute-selector) indicating the attributes that were deleted.\n\n**Example:**\n\n```\n// JS\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  async afterDelete(attributeSelector) {\n    // Don't forget to call the parent method\n    await super.afterDelete(attributeSelector);\n\n    // Implement your custom logic here\n  }\n}\n```\n```\n// TS\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  async afterDelete(attributeSelector: AttributeSelector) {\n    // Don't forget to call the parent method\n    await super.afterDelete(attributeSelector);\n\n    // Implement your custom logic here\n  }\n}\n```\n\n#### Observability\n\nSee the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v1/reference/observable#observable-class) class.\n\n#### Decorators\n\n##### `@attribute([valueType], [options])` <badge type=\"tertiary\">decorator</badge> {#attribute-decorator}\n\nDecorates an attribute of a storable component so it can be combined with a [`Loader`](https://layrjs.com/docs/v1/reference/storable-attribute#loader-type), a [`Finder`](https://layrjs.com/docs/v1/reference/storable-property#finder-type), or any kind of [`Hook`](https://layrjs.com/docs/v1/reference/storable-attribute#hook-type).\n\n**Parameters:**\n\n* `valueType`: A string specifying the [type of values](https://layrjs.com/docs/v1/reference/value-type#supported-types) that can be stored in the attribute (default: `'any'`).\n* `options`: The options to create the [`StorableAttribute`](https://layrjs.com/docs/v1/reference/storable-attribute#constructor).\n\n**Example:**\n\nSee an example of use in the [`StorableAttribute`](https://layrjs.com/docs/v1/reference/storable-attribute) class.\n##### `@primaryIdentifier([valueType], [options])` <badge type=\"tertiary\">decorator</badge> {#primary-identifier-decorator}\n\nDecorates an attribute of a component as a [storable primary identifier attribute](https://layrjs.com/docs/v1/reference/storable-primary-identifier-attribute).\n\n**Parameters:**\n\n* `valueType`: A string specifying the type of values the attribute can store. It can be either `'string'` or `'number'` (default: `'string'`).\n* `options`: The options to create the [`StorablePrimaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/storable-primary-identifier-attribute#constructor).\n\n##### `@secondaryIdentifier([valueType], [options])` <badge type=\"tertiary\">decorator</badge> {#secondary-identifier-decorator}\n\nDecorates an attribute of a component as a [storable secondary identifier attribute](https://layrjs.com/docs/v1/reference/storable-secondary-identifier-attribute).\n\n**Parameters:**\n\n* `valueType`: A string specifying the type of values the attribute can store. It can be either `'string'` or `'number'` (default: `'string'`).\n* `options`: The options to create the [`StorableSecondaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/storable-secondary-identifier-attribute#constructor).\n\n##### `@method([options])` <badge type=\"tertiary\">decorator</badge> {#method-decorator}\n\nDecorates a method of a storable component so it can be combined with a [`Finder`](https://layrjs.com/docs/v1/reference/storable-property#finder-type).\n\n**Parameters:**\n\n* `options`: The options to create the [`StorableMethod`](https://layrjs.com/docs/v1/reference/storable-method#constructor).\n\n**Example:**\n\nSee an example of use in the [`StorableMethod`](https://layrjs.com/docs/v1/reference/storable-method) class.\n##### `@loader(loader)` <badge type=\"tertiary\">decorator</badge> {#loader-decorator}\n\nDecorates a storable attribute with a [`Loader`](https://layrjs.com/docs/v1/reference/storable-attribute#loader-type).\n\n**Parameters:**\n\n* `loader`: A function representing the [`Loader`](https://layrjs.com/docs/v1/reference/storable-attribute#loader-type) of the storable attribute.\n\n**Example:**\n\nSee an example of use in the [`StorableAttribute`](https://layrjs.com/docs/v1/reference/storable-attribute) class.\n##### `@finder(finder)` <badge type=\"tertiary\">decorator</badge> {#finder-decorator}\n\nDecorates a storable attribute or method with a [`Finder`](https://layrjs.com/docs/v1/reference/storable-property#finder-type).\n\n**Parameters:**\n\n* `finder`: A function representing the [`Finder`](https://layrjs.com/docs/v1/reference/storable-property#finder-type) of the storable attribute or method.\n\n**Example:**\n\nSee an example of use in the [`StorableAttribute`](https://layrjs.com/docs/v1/reference/storable-attribute) and [`StorableMethod`](https://layrjs.com/docs/v1/reference/storable-method) classes.\n##### `@index([optionsOrAttributes], [options])` <badge type=\"tertiary\">decorator</badge> {#index-decorator}\n\nDefines an [index](https://layrjs.com/docs/v1/reference/index) for an attribute or a set of attributes.\n\nThis decorator is commonly placed before a storable component attribute to define a [single attribute index](https://layrjs.com/docs/v1/reference/index#single-attribute-indexes), but it can also be placed before a storable component class to define a [compound attribute index](https://layrjs.com/docs/v1/reference/index#compound-attribute-indexes).\n\n**Parameters:**\n\n* `optionsOrAttributes`: Depends on the type of index you want to define (see below).\n* `options`: An object specifying some options in the case of compound attribute index (see below).\n\n###### Single Attribute Indexes\n\nYou can define an index for a single attribute by placing the `@index()` decorator before an attribute definition. In this case, you can specify the following parameters:\n\n- `options`:\n  - `direction`: A string representing the sort direction of the index. The possible values are `'asc'` (ascending) or `'desc'` (descending) and the default value is `'asc'`.\n  - `isUnique`: A boolean specifying whether the index should hold unique values or not (default: `false`). When set to `true`, the underlying database will prevent you to store an attribute with the same value in multiple storable components.\n\n###### Compound Attribute Indexes\n\nYou can define an index that combines multiple attributes by placing the `@index()` decorator before a storable component class definition. In this case, you can specify the following parameters:\n\n- `attributes`: An object specifying the attributes to be indexed. The shape of the object should be `{attributeName: direction, ...}` where `attributeName` is a string representing the name of an attribute and `direction` is a string representing the sort direction (possible values: `'asc'` or `'desc'`).\n- `options`:\n  - `isUnique`: A boolean specifying whether the index should hold unique values or not (default: `false`). When set to `true`, the underlying database will prevent you to store an attribute with the same value in multiple storable components.\n\n**Example:**\n\n```\n// JS\n\nimport {Component} from '@layr/component';\nimport {Storable, attribute, index} from '@layr/storable';\n\n// An index that combines the `year` and `title` attributes:\n@index({year: 'desc', title: 'asc'})\nexport class Movie extends Storable(Component) {\n  // An index for the `title` attribute with the `isUnique` option:\n  @index({isUnique: true}) @attribute('string') title;\n\n  // An index for the `year` attribute with the `'desc'` sort direction:\n  @index({direction: 'desc'}) @attribute('number') year;\n}\n```\n```\n// TS\n\nimport {Component} from '@layr/component';\nimport {Storable, attribute, index} from '@layr/storable';\n\n// An index that combines the `year` and `title` attributes:\n@index({year: 'desc', title: 'asc'})\nexport class Movie extends Storable(Component) {\n  // An index for the `title` attribute with the `isUnique` option:\n  @index({isUnique: true}) @attribute('string') title!: string;\n\n  // An index for the `year` attribute with the `'desc'` sort direction:\n  @index({direction: 'desc'}) @attribute('number') year!: string;\n}\n```\n\n#### Utilities\n\n##### `isStorableClass(value)` <badge type=\"tertiary-outline\">function</badge> {#is-storable-class-function}\n\nReturns whether the specified value is a storable component class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isStorableInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-storable-instance-function}\n\nReturns whether the specified value is a storable component instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isStorableClassOrInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-storable-class-or-instance-function}\n\nReturns whether the specified value is a storable component class or instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `assertIsStorableClass(value)` <badge type=\"tertiary-outline\">function</badge> {#assert-is-storable-class-function}\n\nThrows an error if the specified value is not a storable component class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n##### `assertIsStorableInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#assert-is-storable-instance-function}\n\nThrows an error if the specified value is not a storable component instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n##### `assertIsStorableClassOrInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#assert-is-storable-class-or-instance-function}\n\nThrows an error if the specified value is not a storable component class or instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n##### `ensureStorableClass(component)` <badge type=\"tertiary-outline\">function</badge> {#ensure-storable-class-function}\n\nEnsures that the specified storable component is a class. If you specify a storable component instance (or prototype), the class of the component is returned. If you specify a storable component class, it is returned as is.\n\n**Parameters:**\n\n* `component`: A storable component class or instance.\n\n**Returns:**\n\nA storable component class.\n\n**Example:**\n\n```\nensureStorableClass(movie) => Movie\nensureStorableClass(Movie.prototype) => Movie\nensureStorableClass(Movie) => Movie\n```\n\n##### `ensureStorableInstance(component)` <badge type=\"tertiary-outline\">function</badge> {#ensure-storable-instance-function}\n\nEnsures that the specified storable component is an instance (or prototype). If you specify a storable component class, the component prototype is returned. If you specify a storable component instance (or prototype), it is returned as is.\n\n**Parameters:**\n\n* `component`: A storable component class or instance.\n\n**Returns:**\n\nA storable component instance (or prototype).\n\n**Example:**\n\n```\nensureStorableInstance(Movie) => Movie.prototype\nensureStorableInstance(Movie.prototype) => Movie.prototype\nensureStorableInstance(movie) => movie\n```\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/storable-attribute-7MiyqvA7PUf2E2YeoqA912.immutable.md",
    "content": "### StorableAttribute <badge type=\"primary\">class</badge> {#storable-attribute-class}\n\n*Inherits from [`Attribute`](https://layrjs.com/docs/v1/reference/attribute) and [`StorableProperty`](https://layrjs.com/docs/v1/reference/storable-property).*\n\nThe `StorableAttribute` class extends the [`Attribute`](https://layrjs.com/docs/v1/reference/attribute) class with some capabilities such as [computed attributes](https://layrjs.com/docs/v1/reference/storable-attribute#computed-attributes) or [hooks](https://layrjs.com/docs/v1/reference/storable-attribute#hooks).\n\n#### Usage\n\nTypically, you create a `StorableAttribute` and associate it to a [storable component](https://layrjs.com/docs/v1/reference/storable#storable-component-class) using the [`@attribute()`](https://layrjs.com/docs/v1/reference/storable#attribute-decorator) decorator.\n\nFor example, here is how you would define a `Movie` component with some attributes:\n\n```\n// JS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  @primaryIdentifier() id;\n\n  @attribute('string') title = '';\n\n  @attribute('number') rating;\n\n  @attribute('Date') releaseDate;\n}\n```\n\n```\n// TS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  @primaryIdentifier() id!: string;\n\n  @attribute('string') title = '';\n\n  @attribute('number') rating!: number;\n\n  @attribute('Date') releaseDate!: Date;\n}\n```\n\nSo far we've defined some storable attributes in the same way we would do with [regular attributes](https://layrjs.com/docs/v1/reference/attribute). The only difference is that we imported the [`@attribute()`](https://layrjs.com/docs/v1/reference/storable#attribute-decorator) decorator from `@layr/storable` instead of `@layr/component`.\n\nLet's now see how to take advantage of some capabilities that are unique to storable attributes.\n\n##### Computed Attributes\n\nA computed attribute is a special kind of component attribute that computes its value when the component is loaded with a storable component method such as [`load()`](https://layrjs.com/docs/v1/reference/storable#load-instance-method), [`get()`](https://layrjs.com/docs/v1/reference/storable#get-class-method), or [`find()`](https://layrjs.com/docs/v1/reference/storable#find-class-method).\n\nThe value of a computed attribute shouldn't be set manually, and is not persisted when you [save](https://layrjs.com/docs/v1/reference/storable#save-instance-method) a component to a store.\n\n###### Loaders\n\nUse the [`@loader()`](https://layrjs.com/docs/v1/reference/storable#loader-decorator) decorator to specify the function that computes the value of a computed attribute.\n\nFor example, let's define a `isTrending` computed attribute that determines its value according to the movie's `rating` and `releaseDate`:\n\n```\n// JS\n\nimport {loader} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  @loader(async function() {\n    await this.load({rating: true, releaseDate: true});\n\n    const ratingLimit = 7;\n    const releaseDateLimit = new Date(Date.now() - 864000000); // 10 days before\n\n    return this.rating >= ratingLimit && this.releaseDate >= releaseDateLimit;\n  })\n  @attribute('boolean')\n  isTrending;\n}\n```\n\n```\n// TS\n\nimport {loader} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  @loader(async function(this: Movie) {\n    await this.load({rating: true, releaseDate: true});\n\n    const ratingLimit = 7;\n    const releaseDateLimit = new Date(Date.now() - 864000000); // 10 days before\n\n    return this.rating >= ratingLimit && this.releaseDate >= releaseDateLimit;\n  })\n  @attribute('boolean')\n  isTrending!: boolean;\n}\n```\n\nThen, when we get a movie, we can get the `isTrending` computed attribute like any attribute:\n\n```\nconst movie = await Movie.get('abc123', {title: true, isTrending: true});\n\nmovie.title; // => 'Inception'\nmovie.isTrending; // => true (on July 16th, 2010)\n```\n\n###### Finders\n\nThe best thing about computed attributes is that they can be used in a [`Query`](https://layrjs.com/docs/v1/reference/query) when you are [finding](https://layrjs.com/docs/v1/reference/storable#find-class-method) or [counting](https://layrjs.com/docs/v1/reference/storable#count-class-method) some storable components.\n\nTo enable that, use the [`@finder()`](https://layrjs.com/docs/v1/reference/storable#finder-decorator) decorator, and specify a function that returns a [`Query`](https://layrjs.com/docs/v1/reference/query).\n\nFor example, let's make our `isTrending` attribute searchable by adding a finder:\n\n```\n// JS\n\nimport {loader} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  @loader(\n    // ...\n  )\n  @finder(function (isTrending) {\n    const ratingLimit = 7;\n    const releaseDateLimit = new Date(Date.now() - 864000000); // 10 days before\n\n    if (isTrending) {\n      // Return a query for `{isTrending: true}`\n      return {\n        rating: {$greaterThanOrEqual: ratingLimit}\n        releaseDate: {$greaterThanOrEqual: releaseDateLimit}\n      };\n    }\n\n    // Return a query for `{isTrending: false}`\n    return {\n      $or: [\n        {rating: {$lessThan: ratingLimit}},\n        {releaseDate: {$lessThan: releaseDateLimit}}\n      ]\n    };\n  })\n  @attribute('boolean')\n  isTrending;\n}\n```\n\n```\n// TS\n\nimport {loader} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  @loader(\n    // ...\n  )\n  @finder(function (isTrending: boolean) {\n    const ratingLimit = 7;\n    const releaseDateLimit = new Date(Date.now() - 864000000); // 10 days before\n\n    if (isTrending) {\n      // Return a query for `{isTrending: true}`\n      return {\n        rating: {$greaterThanOrEqual: ratingLimit}\n        releaseDate: {$greaterThanOrEqual: releaseDateLimit}\n      };\n    }\n\n    // Return a query for `{isTrending: false}`\n    return {\n      $or: [\n        {rating: {$lessThan: ratingLimit}},\n        {releaseDate: {$lessThan: releaseDateLimit}}\n      ]\n    };\n  })\n  @attribute('boolean')\n  isTrending!: boolean;\n}\n```\n\nAnd now, we can query our `isTrending` computed attribute like we would do with any attribute:\n\n```\nawait Movie.find({isTrending: true}); // => All trending movies\nawait Movie.find({isTrending: false}); // => All non-trending movies\n\nawait Movie.count({isTrending: true}); // => Number of trending movies\nawait Movie.count({isTrending: false}); // => Number of non-trending movies\n\n// Combine computed attributes with regular attributes to find\n// all Japanese trending movies\nawait Movie.find({country: 'Japan', isTrending: true});\n```\n\n##### Hooks\n\nStorable attributes offer a number of hooks that you can use to execute some custom logic when an attribute is loaded, saved or deleted.\n\nTo define a hook for a storable attribute, use one of the following [`@attribute()`](https://layrjs.com/docs/v1/reference/storable#attribute-decorator) options:\n\n- `beforeLoad`: Specifies a [`Hook`](https://layrjs.com/docs/v1/reference/storable-attribute#hook-type) to be executed *before* an attribute is *loaded*.\n- `afterLoad`: Specifies a [`Hook`](https://layrjs.com/docs/v1/reference/storable-attribute#hook-type) to be executed *after* an attribute is *loaded*.\n- `beforeSave`: Specifies a [`Hook`](https://layrjs.com/docs/v1/reference/storable-attribute#hook-type) to be executed *before* an attribute is *saved*.\n- `afterSave`: Specifies a [`Hook`](https://layrjs.com/docs/v1/reference/storable-attribute#hook-type) to be executed *after* an attribute is *saved*.\n- `beforeDelete`: Specifies a [`Hook`](https://layrjs.com/docs/v1/reference/storable-attribute#hook-type) to be executed *before* an attribute is *deleted*.\n- `afterDelete`: Specifies a [`Hook`](https://layrjs.com/docs/v1/reference/storable-attribute#hook-type) to be executed *after* an attribute is *deleted*.\n\nFor example, we could use a `beforeSave` hook to make sure a user's password is hashed before it is saved to a store:\n\n```\n// JS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\nimport bcrypt from 'bcryptjs';\n\nclass User extends Storable(Component) {\n  @primaryIdentifier() id;\n\n  @attribute('string') username;\n\n  @attribute('string', {\n    async beforeSave() {\n      this.password = await bcrypt.hash(this.password);\n    }\n  })\n  password;\n}\n```\n\n```\n// TS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\nimport bcrypt from 'bcryptjs';\n\nclass User extends Storable(Component) {\n  @primaryIdentifier() id!: string;\n\n  @attribute('string') username!: string;\n\n  @attribute('string', {\n    async beforeSave(this: User) {\n      this.password = await bcrypt.hash(this.password);\n    }\n  })\n  password!: string;\n}\n```\n\nThen, when we save a user, its password gets automatically hashed:\n```\nconst user = new User({username: 'steve', password: 'zyx98765'});\n\nuser.password; // => 'zyx98765'\n\nawait user.save(); // The password will be hashed before saved to the store\n\nuser.password; // => '$2y$12$AGJ91pnqlM7TcqnLg0iIFuiN80z9k.wFnGVl1a4lrANUepBKmvNVO'\n\n// Note that if we save the user again, as long as its password hasn't changed,\n// it will not be saved, and therefore not be hashed again\n\nuser.username = 'steve2';\nawait user.save(); // Only the username will be saved\n\nuser.password; // => '$2y$12$AGJ91pnqlM7TcqnLg0iIFuiN80z9k.wFnGVl1a4lrANUepBKmvNVO'\n```\n\n#### Creation\n\n##### `new StorableAttribute(name, parent, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates a storable attribute. Typically, instead of using this constructor, you would rather use the [`@attribute()`](https://layrjs.com/docs/v1/reference/storable#attribute-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the attribute.\n* `parent`: The [storable component](https://layrjs.com/docs/v1/reference/storable#storable-component-class) class, prototype, or instance that owns the attribute.\n* `options`:\n  * `valueType`: A string specifying the [type of values](https://layrjs.com/docs/v1/reference/value-type#supported-types) the attribute can store (default: `'any'`).\n  * `default`: The default value (or a function returning the default value) of the attribute.\n  * `validators`: An array of [validators](https://layrjs.com/docs/v1/reference/validator) for the value of the attribute.\n  * `items`:\n    * `validators`: An array of [validators](https://layrjs.com/docs/v1/reference/validator) for the items of an array attribute.\n  * `loader`: A function specifying a [`Loader`](https://layrjs.com/docs/v1/reference/storable-attribute#loader-type) for the attribute.\n  * `finder`: A function specifying a [`Finder`](https://layrjs.com/docs/v1/reference/storable-property#finder-type) for the attribute.\n  * `beforeLoad`: A function specifying a \"beforeLoad\" [`hook`](https://layrjs.com/docs/v1/reference/storable-attribute#hook-type) for the attribute.\n  * `afterLoad`: A function specifying an \"afterLoad\" [`hook`](https://layrjs.com/docs/v1/reference/storable-attribute#hook-type) for the attribute.\n  * `beforeSave`: A function specifying a \"beforeSave\" [`hook`](https://layrjs.com/docs/v1/reference/storable-attribute#hook-type) for the attribute.\n  * `afterSave`: A function specifying an \"afterSave\" [`hook`](https://layrjs.com/docs/v1/reference/storable-attribute#hook-type) for the attribute.\n  * `beforeDelete`: A function specifying a \"beforeDelete\" [`hook`](https://layrjs.com/docs/v1/reference/storable-attribute#hook-type) for the attribute.\n  * `afterDelete`: A function specifying an \"afterDelete\" [`hook`](https://layrjs.com/docs/v1/reference/storable-attribute#hook-type) for the attribute.\n  * `exposure`: A [`PropertyExposure`](https://layrjs.com/docs/v1/reference/property#property-exposure-type) object specifying how the attribute should be exposed to remote access.\n\n**Returns:**\n\nThe [`StorableAttribute`](https://layrjs.com/docs/v1/reference/storable-attribute) instance that was created.\n\n#### Property Methods\n\nSee the methods that are inherited from the [`Property`](https://layrjs.com/docs/v1/reference/property#basic-methods) class.\n\n#### Attribute Methods\n\nSee the methods that are inherited from the [`Attribute`](https://layrjs.com/docs/v1/reference/attribute#value-type) class.\n\n#### Loader\n\n##### `getLoader()` <badge type=\"secondary-outline\">instance method</badge> {#get-loader-instance-method}\n\nReturns the [`Loader`](https://layrjs.com/docs/v1/reference/storable-attribute#loader-type) of the attribute.\n\n**Returns:**\n\nA [`Loader`](https://layrjs.com/docs/v1/reference/storable-attribute#loader-type) function (or `undefined` if the attribute has no associated loader).\n\n##### `hasLoader()` <badge type=\"secondary-outline\">instance method</badge> {#has-loader-instance-method}\n\nReturns whether the attribute has a [`Loader`](https://layrjs.com/docs/v1/reference/storable-attribute#loader-type).\n\n**Returns:**\n\nA boolean.\n\n##### `setLoader(loader)` <badge type=\"secondary-outline\">instance method</badge> {#set-loader-instance-method}\n\nSets a [`Loader`](https://layrjs.com/docs/v1/reference/storable-attribute#loader-type) for the attribute.\n\n**Parameters:**\n\n* `loader`: The [`Loader`](https://layrjs.com/docs/v1/reference/storable-attribute#loader-type) function to set.\n\n##### `Loader` <badge type=\"primary-outline\">type</badge> {#loader-type}\n\nA function representing the \"loader\" of an attribute.\n\nThe function should return a value for the attribute that is being loaded. Typically, you would return a value according to the value of some other attributes.\n\nThe function can be `async` and is executed with the parent of the attribute as `this` context.\n\nSee an example of use in the [\"Computed Attributes\"](https://layrjs.com/docs/v1/reference/storable-attribute#computed-attributes) section above.\n\n#### Finder\n\nSee the methods that are inherited from the [`StorableProperty`](https://layrjs.com/docs/v1/reference/storable-property#finder) class.\n\n#### Hooks\n\n##### `getHook(name)` <badge type=\"secondary-outline\">instance method</badge> {#get-hook-instance-method}\n\nReturns a specific [`Hook`](https://layrjs.com/docs/v1/reference/storable-attribute#hook-type) for the current attribute.\n\n**Parameters:**\n\n* `name`: A string representing the name of the hook you want to get. The possible values are `'beforeLoad'`, `'afterLoad'`, `'beforeSave'`, `'afterSave'`, `'beforeDelete'`, and `'afterDelete'`.\n\n**Returns:**\n\nA [`Hook`](https://layrjs.com/docs/v1/reference/storable-attribute#hook-type) function (or `undefined` if the attribute doesn't have a hook with the specified `name`).\n\n##### `hasHook(name)` <badge type=\"secondary-outline\">instance method</badge> {#has-hook-instance-method}\n\nReturns whether the current attribute has a specific [`Hook`](https://layrjs.com/docs/v1/reference/storable-attribute#hook-type).\n\n**Parameters:**\n\n* `name`: A string representing the name of the hook you want to check. The possible values are `'beforeLoad'`, `'afterLoad'`, `'beforeSave'`, `'afterSave'`, `'beforeDelete'`, and `'afterDelete'`.\n\n**Returns:**\n\nA boolean.\n\n##### `setHook(name, hook)` <badge type=\"secondary-outline\">instance method</badge> {#set-hook-instance-method}\n\nSets a [`Hook`](https://layrjs.com/docs/v1/reference/storable-attribute#hook-type) for the current attribute.\n\n**Parameters:**\n\n* `name`: A string representing the name of the hook you want to set. The possible values are `'beforeLoad'`, `'afterLoad'`, `'beforeSave'`, `'afterSave'`, `'beforeDelete'`, and `'afterDelete'`.\n* `hook`: The [`Hook`](https://layrjs.com/docs/v1/reference/storable-attribute#hook-type) function to set.\n\n##### `Hook` <badge type=\"primary-outline\">type</badge> {#hook-type}\n\nA function representing a \"hook\" of an attribute.\n\nAccording to the type of the hook, the function is automatically called when an attribute is loaded, saved or deleted.\n\nThe function can be `async` and is invoked with the parent of the attribute as `this` context.\n\nSee an example of use in the [\"Hooks\"](https://layrjs.com/docs/v1/reference/storable-attribute#hooks) section above.\n\n#### Observability\n\nSee the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v1/reference/observable#observable-class) class.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/storable-method-1rnW3aKFlFYZ3kIJdCGXRv.immutable.md",
    "content": "### StorableMethod <badge type=\"primary\">class</badge> {#storable-method-class}\n\n*Inherits from [`Method`](https://layrjs.com/docs/v1/reference/method) and [`StorableProperty`](https://layrjs.com/docs/v1/reference/storable-property).*\n\nThe `StorableMethod` class extends the [`Method`](https://layrjs.com/docs/v1/reference/method) class with the capabilities of the [`StorableProperty`](https://layrjs.com/docs/v1/reference/storable-property) class.\n\nIn a nutshell, using the `StorableMethod` class allows you to associate a [`Finder`](https://layrjs.com/docs/v1/reference/storable-property#finder-type) to a method so this method can be used in a [`Query`](https://layrjs.com/docs/v1/reference/query).\n\n\n#### Usage\n\nTypically, you create a `StorableMethod` and associate it to a [storable component](https://layrjs.com/docs/v1/reference/storable#storable-component-class) using the [`@method()`](https://layrjs.com/docs/v1/reference/storable#method-decorator) decorator.\n\nFor example, here is how you would define a `Movie` component with some storable attributes and methods:\n\n```\n// JS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute, method} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  @primaryIdentifier() id;\n\n  @attribute('string') title = '';\n\n  @attribute('string') country = '';\n\n  @attribute('Date') releaseDate;\n\n  @method() async wasReleasedIn(year) {\n    await this.load({releaseDate});\n\n    return this.releaseDate().getFullYear() === year;\n  }\n}\n```\n\n```\n// TS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute, method} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  @primaryIdentifier() id!: string;\n\n  @attribute('string') title = '';\n\n  @attribute('string') country = '';\n\n  @attribute('Date') releaseDate!: Date;\n\n  @method() async wasReleasedIn(year: number) {\n    await this.load({releaseDate});\n\n    return this.releaseDate().getUTCFullYear() === year;\n  }\n}\n```\n\nNotice the `wasReleasedIn()` method that allows us to determine if a movie was released in a specific year. We could use this method as follows:\n\n```\nconst movie = new Movie({\n  title: 'Inception',\n  country: 'USA',\n  releaseDate: new Date('2010-07-16')\n});\n\nawait movie.wasReleasedIn(2010); // => true\nawait movie.wasReleasedIn(2011); // => false\n```\n\nSo far, there is nothing special about the `wasReleasedIn()` method. We could have achieved the same result without the [`@method()`](https://layrjs.com/docs/v1/reference/storable#method-decorator) decorator.\n\nNow, let's imagine that we want to find all the movies that was released in 2010. We could do so as follows:\n\n```\nawait Movie.find({\n  releaseDate: {\n    $greaterThanOrEqual: new Date('2010-01-01'),\n    $lessThan: new Date('2011-01-01')\n  }\n});\n```\n\nThat would certainly work, but wouldn't it be great if we could do the following instead:\n\n```\nawait Movie.find({wasReleasedIn: 2010});\n```\n\nUnfortunately, the above [`Query`](https://layrjs.com/docs/v1/reference/query) wouldn't work. To make such a query possible, we must somehow transform the logic of the `wasReleasedIn()` method into a regular query, and this is exactly where a `StorableMethod` can be useful.\n\nBecause the `wasReleasedIn()` method is a `StorableMethod` (thanks to the [`@method()`](https://layrjs.com/docs/v1/reference/storable#method-decorator) decorator), we can can associate a [`Finder`](https://layrjs.com/docs/v1/reference/storable-property#finder-type) to it by adding the [`@finder()`](https://layrjs.com/docs/v1/reference/storable#finder-decorator) decorator:\n\n```\n// JS\n\n// ...\n\nimport {finder} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  @finder(function (year) {\n    return {\n      releaseDate: {\n        $greaterThanOrEqual: new Date(`${year}-01-01`),\n        $lessThan: new Date(`${year + 1}-01-01`)\n      }\n    };\n  })\n  @method()\n  async wasReleasedIn(year) {\n    // ...\n  }\n}\n```\n\n```\n// TS\n\n// ...\n\nimport {finder} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  @finder(function (year: number) {\n    return {\n      releaseDate: {\n        $greaterThanOrEqual: new Date(`${year}-01-01`),\n        $lessThan: new Date(`${year + 1}-01-01`)\n      }\n    };\n  })\n  @method()\n  async wasReleasedIn(year: number) {\n    // ...\n  }\n}\n```\n\nAnd now, it is possible to use the `wasReleasedIn()` method in any query:\n\n```\n// Find all the movies released in 2010\nawait Movie.find({wasReleasedIn: 2010});\n\n// Find all the American movies released in 2010\nawait Movie.find({country: 'USA', wasReleasedIn: 2010});\n```\n\n#### Creation\n\n##### `new StorableMethod(name, parent, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates a storable method. Typically, instead of using this constructor, you would rather use the [`@method()`](https://layrjs.com/docs/v1/reference/storable#method-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the method.\n* `parent`: The [storable component](https://layrjs.com/docs/v1/reference/storable#storable-component-class) class, prototype, or instance that owns the method.\n* `options`: An object specifying any option supported by the constructor of [`Method`](https://layrjs.com/docs/v1/reference/method#constructor) and [`StorableProperty`](https://layrjs.com/docs/v1/reference/storable-property#constructor).\n\n**Returns:**\n\nThe [`StorableMethod`](https://layrjs.com/docs/v1/reference/storable-method) instance that was created.\n\n#### Property Methods\n\nSee the methods that are inherited from the [`Property`](https://layrjs.com/docs/v1/reference/property#basic-methods) class.\n\n#### Finder\n\nSee the methods that are inherited from the [`StorableProperty`](https://layrjs.com/docs/v1/reference/storable-property#finder) class.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/storable-primary-identifier-attribute-74IwGTycdA7NVAybBly3t1.immutable.md",
    "content": "### StorablePrimaryIdentifierAttribute <badge type=\"primary\">class</badge> {#storable-primary-identifier-attribute-class}\n\n*Inherits from [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/primary-identifier-attribute) and [`StorableAttribute`](https://layrjs.com/docs/v1/reference/storable-attribute).*\n\nThe `StorablePrimaryIdentifierAttribute` class is like the [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/primary-identifier-attribute) class but extended with the capabilities of the [`StorableAttribute`](https://layrjs.com/docs/v1/reference/storable-attribute) class.\n\n#### Usage\n\nTypically, you create a `StorablePrimaryIdentifierAttribute` and associate it to a [storable component](https://layrjs.com/docs/v1/reference/storable#storable-component-class) using the [`@primaryIdentifier()`](https://layrjs.com/docs/v1/reference/storable#primary-identifier-decorator) decorator.\n\n**Example:**\n\n```\n// JS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  @primaryIdentifier() id;\n\n  @attribute('string') title = '';\n}\n```\n\n```\n// TS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  @primaryIdentifier() id!: string;\n\n  @attribute('string') title = '';\n}\n```\n\n#### Creation\n\n##### `new StorablePrimaryIdentifierAttribute(name, parent, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates a storable primary identifier attribute. Typically, instead of using this constructor, you would rather use the [`@primaryIdentifier()`](https://layrjs.com/docs/v1/reference/storable#primary-identifier-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the attribute.\n* `parent`: The [storable component](https://layrjs.com/docs/v1/reference/storable#storable-component-class) prototype that owns the attribute.\n* `options`: An object specifying any option supported by the constructor of [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/primary-identifier-attribute#constructor) and [`StorableAttribute`](https://layrjs.com/docs/v1/reference/storable-attribute#constructor).\n\n**Returns:**\n\nThe [`StorablePrimaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/storable-primary-identifier-attribute) instance that was created.\n\n#### Property Methods\n\nSee the methods that are inherited from the [`Property`](https://layrjs.com/docs/v1/reference/property#basic-methods) class.\n\n#### Attribute Methods\n\nSee the methods that are inherited from the [`Attribute`](https://layrjs.com/docs/v1/reference/attribute#value-type) class.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/storable-property-5LuWR7uuQ1qdebK3iszZJO.immutable.md",
    "content": "### StorableProperty <badge type=\"primary\">class</badge> {#storable-property-class}\n\n*Inherits from [`Property`](https://layrjs.com/docs/v1/reference/property).*\n\nA base class from which classes such as [`StorableAttribute`](https://layrjs.com/docs/v1/reference/storable-attribute) or [`StorableMethod`](https://layrjs.com/docs/v1/reference/storable-method) are constructed. Unless you build a custom property class, you probably won't have to use this class directly.\n\n#### Creation\n\n##### `new StorableProperty(name, parent, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates a storable property.\n\n**Parameters:**\n\n* `name`: The name of the property.\n* `parent`: The [storable component](https://layrjs.com/docs/v1/reference/storable#storable-component-class) class, prototype, or instance that owns the property.\n* `options`:\n  * `finder`: A function specifying a [`Finder`](https://layrjs.com/docs/v1/reference/storable-property#finder-type) for the property.\n  * `exposure`: A [`PropertyExposure`](https://layrjs.com/docs/v1/reference/property#property-exposure-type) object specifying how the property should be exposed to remote access.\n\n**Returns:**\n\nThe [`StorableProperty`](https://layrjs.com/docs/v1/reference/storable-property) instance that was created.\n\n#### Property Methods\n\nSee the methods that are inherited from the [`Property`](https://layrjs.com/docs/v1/reference/property#basic-methods) class.\n\n#### Finder\n\n##### `getFinder()` <badge type=\"secondary-outline\">instance method</badge> {#get-finder-instance-method}\n\nReturns the [`Finder`](https://layrjs.com/docs/v1/reference/storable-property#finder-type)of  the property.\n\n**Returns:**\n\nA [`Finder`](https://layrjs.com/docs/v1/reference/storable-property#finder-type) function (or `undefined` if the property has no associated finder).\n\n##### `hasFinder()` <badge type=\"secondary-outline\">instance method</badge> {#has-finder-instance-method}\n\nReturns whether the property has a [`Finder`](https://layrjs.com/docs/v1/reference/storable-property#finder-type).\n\n**Returns:**\n\nA boolean.\n\n##### `setFinder(finder)` <badge type=\"secondary-outline\">instance method</badge> {#set-finder-instance-method}\n\nSets a [`Finder`](https://layrjs.com/docs/v1/reference/storable-property#finder-type) for the property.\n\n**Parameters:**\n\n* `finder`: The [`Finder`](https://layrjs.com/docs/v1/reference/storable-property#finder-type) function to set.\n\n##### `Finder` <badge type=\"primary-outline\">type</badge> {#finder-type}\n\nA function representing the \"finder\" of a property.\n\nThe function should return a [`Query`](https://layrjs.com/docs/v1/reference/query) for the property that is queried for.\n\nThe function has the following characteristics:\n\n- It can be `async`.\n- As first parameter, it receives the value that was specified in the user's query.\n- It is executed with the parent of the property as `this` context.\n\nSee an example of use in the [`StorableAttribute`](https://layrjs.com/docs/v1/reference/storable-attribute) and [`StorableMethod`](https://layrjs.com/docs/v1/reference/storable-method) classes.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/storable-secondary-identifier-attribute-6CqhR0kLxoLhtWU85EcsZU.immutable.md",
    "content": "### StorableSecondaryIdentifierAttribute <badge type=\"primary\">class</badge> {#storable-secondary-identifier-attribute-class}\n\n*Inherits from [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/secondary-identifier-attribute) and [`StorableAttribute`](https://layrjs.com/docs/v1/reference/storable-attribute).*\n\nThe `StorableSecondaryIdentifierAttribute` class is like the [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/secondary-identifier-attribute) class but extended with the capabilities of the [`StorableAttribute`](https://layrjs.com/docs/v1/reference/storable-attribute) class.\n\n#### Usage\n\nTypically, you create a `StorableSecondaryIdentifierAttribute` and associate it to a [storable component](https://layrjs.com/docs/v1/reference/storable#storable-component-class) using the [`@secondaryIdentifier()`](https://layrjs.com/docs/v1/reference/storable#secondary-identifier-decorator) decorator.\n\n**Example:**\n\n```\n// JS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, secondaryIdentifier, attribute} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  @primaryIdentifier() id;\n\n  @secondaryIdentifier() slug;\n\n  @attribute('string') title = '';\n}\n```\n\n```\n// TS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, secondaryIdentifier, attribute} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  @primaryIdentifier() id!: string;\n\n  @secondaryIdentifier() slug!: string;\n\n  @attribute('string') title = '';\n}\n```\n\n#### Creation\n\n##### `new StorableSecondaryIdentifierAttribute(name, parent, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates a storable secondary identifier attribute. Typically, instead of using this constructor, you would rather use the [`@secondaryIdentifier()`](https://layrjs.com/docs/v1/reference/storable#secondary-identifier-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the attribute.\n* `parent`: The [storable component](https://layrjs.com/docs/v1/reference/storable#storable-component-class) prototype that owns the attribute.\n* `options`: An object specifying any option supported by the constructor of [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/secondary-identifier-attribute#constructor) and [`StorableAttribute`](https://layrjs.com/docs/v1/reference/storable-attribute#constructor).\n\n**Returns:**\n\nThe [`StorableSecondaryIdentifierAttribute`](https://layrjs.com/docs/v1/reference/storable-secondary-identifier-attribute) instance that was created.\n\n#### Property Methods\n\nSee the methods that are inherited from the [`Property`](https://layrjs.com/docs/v1/reference/property#basic-methods) class.\n\n#### Attribute Methods\n\nSee the methods that are inherited from the [`Attribute`](https://layrjs.com/docs/v1/reference/attribute#value-type) class.\n\n#### Observability\n\nSee the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v1/reference/observable#observable-class) class.\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/store-6BvlCSWkAb7smHsvoIGjv1.immutable.md",
    "content": "### Store <badge type=\"primary\">class</badge> {#store-class}\n\nAn abstract class from which classes such as [`MongoDBStore`](https://layrjs.com/docs/v1/reference/mongodb-store) or [`MemoryStore`](https://layrjs.com/docs/v1/reference/memory-store) are constructed. Unless you build a custom store, you probably won't have to use this class directly.\n\n#### Component Registration\n\n##### `registerRootComponent(rootComponent)` <badge type=\"secondary-outline\">instance method</badge> {#register-root-component-instance-method}\n\nRegisters all the [storable components](https://layrjs.com/docs/v1/reference/storable#storable-component-class) that are provided (directly or recursively) by the specified root component.\n\n**Parameters:**\n\n* `rootComponent`: A [`Component`](https://layrjs.com/docs/v1/reference/component) class.\n\n**Example:**\n\n```\nimport {Component} from '@layr/component';\nimport {Storable} from '@layr/storable';\nimport {MongoDBStore} from '@layr/mongodb-store';\n\nclass User extends Storable(Component) {\n  // ...\n}\n\nclass Movie extends Storable(Component) {\n  // ...\n}\n\nclass Backend extends Component {\n  @provide() static User = User;\n  @provide() static Movie = Movie;\n}\n\nconst store = new MongoDBStore('mongodb://user:pass@host:port/db');\n\nstore.registerRootComponent(Backend); // User and Movie will be registered\n```\n\n##### `getRootComponents()` <badge type=\"secondary-outline\">instance method</badge> {#get-root-components-instance-method}\n\nGets all the root components that are registered into the store.\n\n**Returns:**\n\nAn iterator of [`Component`](https://layrjs.com/docs/v1/reference/component) classes.\n\n##### `getStorable(name)` <badge type=\"secondary-outline\">instance method</badge> {#get-storable-instance-method}\n\nGets a [storable component](https://layrjs.com/docs/v1/reference/storable#storable-component-class) that is registered into the store. An error is thrown if there is no storable component with the specified name.\n\n**Parameters:**\n\n* `name`: The name of the storable component to get.\n\n**Returns:**\n\nA [`StorableComponent`](https://layrjs.com/docs/v1/reference/storable#storable-component-class) class.\n\n**Example:**\n\n```\n// See the definition of `store` in the `registerRootComponent()` example\n\nstore.getStorable('Movie'); // => Movie class\nstore.getStorable('User'); // => User class\nstore.getStorable('Film'); // => Error\n```\n\n##### `hasStorable(name)` <badge type=\"secondary-outline\">instance method</badge> {#has-storable-instance-method}\n\nReturns whether a [storable component](https://layrjs.com/docs/v1/reference/storable#storable-component-class) is registered into the store.\n\n**Parameters:**\n\n* `name`: The name of the storable component to check.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\n// See the definition of `store` in the `registerRootComponent()` example\n\nstore.hasStorable('Movie'); // => true\nstore.hasStorable('User'); // => true\nstore.hasStorable('Film'); // => false\n```\n\n##### `registerStorable(storable)` <badge type=\"secondary-outline\">instance method</badge> {#register-storable-instance-method}\n\nRegisters a specific [storable component](https://layrjs.com/docs/v1/reference/storable#storable-component-class) into the store. Typically, instead of using this method, you would rather use the [`registerRootComponent()`](https://layrjs.com/docs/v1/reference/store#register-root-component-instance-method) method to register multiple storable components at once.\n\n**Parameters:**\n\n* `storable`: The [`StorableComponent`](https://layrjs.com/docs/v1/reference/storable#storable-component-class) class to register.\n\n**Example:**\n\n```\nclass Movie extends Storable(Component) {\n  // ...\n}\n\nconst store = new MongoDBStore('mongodb://user:pass@host:port/db');\n\nstore.registerStorable(Movie);\n```\n\n##### `getStorables()` <badge type=\"secondary-outline\">instance method</badge> {#get-storables-instance-method}\n\nGets all the [storable components](https://layrjs.com/docs/v1/reference/storable#storable-component-class) that are registered into the store.\n\n**Returns:**\n\nAn iterator of [`StorableComponent`](https://layrjs.com/docs/v1/reference/storable#storable-component-class) classes.\n\n#### Migration\n\n##### `migrateStorables([options])` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#migrate-storables-instance-method}\n\nMigrates the database to reflect all the [storable components](https://layrjs.com/docs/v1/reference/storable#storable-component-class) that are registered into the store.\n\nThe migration consists in synchronizing the indexes of the database with the indexes that are defined in each storable component (typically using the [`@index()`](https://layrjs.com/docs/v1/reference/storable#index-decorator) decorator).\n\n**Parameters:**\n\n* `options`:\n  * `silent`: A boolean specifying whether the operation should not produce any output in the console (default: `false`).\n\n**Example:**\n\nSee an example of use in the [`Index`](https://layrjs.com/docs/v1/reference/index) class."
  },
  {
    "path": "website/frontend/public/docs/v1/reference/validator-2strlvnapeTZmbWT3o3VM1.immutable.md",
    "content": "### Validator <badge type=\"primary\">class</badge> {#validator-class}\n\nA class to handle the validation of the component attributes.\n\n#### Usage\n\nYou shouldn't have to create a `Validator` instance directly. Instead, when you define an attribute (using a decorator such as [`@attribute()`](https://layrjs.com/docs/v1/reference/component#attribute-decorator)), you can invoke some [built-in validator builders](https://layrjs.com/docs/v1/reference/validator#built-in-validator-builders) or specify your own [custom validation functions](https://layrjs.com/docs/v1/reference/validator#custom-validation-functions) that will be automatically transformed into `Validator` instances.\n\n**Example:**\n\n```\n// JS\n\nimport {Component, attribute, validators} from '@layr/component';\n\nconst {notEmpty, maxLength, integer, greaterThan} = validators;\n\nclass Movie extends Component {\n  // An attribute of type 'string' that cannot be empty or exceed 30 characters\n  @attribute('string', {validators: [notEmpty(), maxLength(30)]}) title;\n\n  // An attribute of type 'number' that must an integer greater than 0\n  @attribute('number', {validators: [integer(), greaterThan(0)]}) reference;\n\n  // An array attribute that can contain up to 5 non-empty strings\n  @attribute('string[]', {validators: [maxLength(5)], items: {validators: [notEmpty()]}})\n  tags = [];\n}\n```\n\n```\n// TS\n\nimport {Component, attribute, validators} from '@layr/component';\n\nconst {notEmpty, maxLength, integer, greaterThan} = validators;\n\nclass Movie extends Component {\n  // An attribute of type 'string' that cannot be empty or exceed 30 characters\n  @attribute('string', {validators: [notEmpty(), maxLength(30)]}) title!: string;\n\n  // An attribute of type 'number' that must an integer greater than 0\n  @attribute('number', {validators: [integer(), greaterThan(0)]}) reference!: number;\n\n  // An array attribute that can contain up to 5 non-empty strings\n  @attribute('string[]', {validators: [maxLength(5)], items: {validators: [notEmpty()]}})\n  tags: string[] = [];\n}\n```\n\nIn case you want to access the `Validator` instances that were created under the hood, you can do the following:\n\n```\nconst movie = new Movie({ ... });\n\nmovie.getAttribute('title').getValueType().getValidators();\n// => [notEmptyValidator, maxLengthValidator]\n\nmovie.getAttribute('reference').getValueType().getValidators();\n// => [integerValidator, greaterThanValidator]\n\nmovie.getAttribute('tags').getValueType().getValidators();\n// => [maxLengthValidator]\n\nmovie.getAttribute('tags').getValueType().getItemType().getValidators();\n// => [notEmptyValidator]\n```\n\n#### Built-In Validator Builders\n\nLayr provides a number of validator builders that can be used when you define your component attributes. See an [example of use](https://layrjs.com/docs/v1/reference/validator#usage) above.\n\n##### Numbers\n\nThe following validator builders can be used to validate numbers:\n\n* `integer()`: Ensures that a number is an integer.\n* `positive()`: Ensures that a number is greater than or equal to 0.\n* `negative()`: Ensures that a number is less than 0.\n* `lessThan(value)`: Ensures that a number is less than the specified value.\n* `lessThanOrEqual(value)`: Ensures that a number is less than or equal to the specified value.\n* `greaterThan(value)`: Ensures that a number is greater than the specified value.\n* `greaterThanOrEqual(value)`: Ensures that a number is greater than or equal to the specified value.\n* `range([min, max])`: Ensures that a number is in the specified inclusive range.\n* `anyOf(arrayOfNumbers)`: Ensures that a number is any of the specified numbers.\n* `noneOf(arrayOfNumbers)`: Ensures that a number is none of the specified numbers.\n\n##### Strings\n\nThe following validator builders can be used to validate strings:\n\n* `notEmpty()`: Ensures that a string is not empty.\n* `minLength(value)`: Ensures that a string has at least the specified number of characters.\n* `maxLength(value)`: Ensures that a string doesn't exceed the specified number of characters.\n* `rangeLength([min, max])`: Ensures that the length of a string is in the specified inclusive range.\n* `match(regExp)`: Ensures that a string matches the specified [regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions).\n* `anyOf(arrayOfStrings)`: Ensures that a string is any of the specified strings.\n* `noneOf(arrayOfStrings)`: Ensures that a string is none of the specified strings.\n\n##### Arrays\n\nThe following validator builders can be used to validate arrays:\n\n* `notEmpty()`: Ensures that an array is not empty.\n* `minLength(value)`: Ensures that an array has at least the specified number of items.\n* `maxLength(value)`: Ensures that an array doesn't exceed the specified number of items.\n* `rangeLength([min, max])`: Ensures that the length of an array is in the specified inclusive range.\n\n##### Any Type\n\nThe following validator builder can be used to validate any type of values:\n\n* `required()`: Ensures that a value is not undefined.\n* `missing()`: Ensures that a value is undefined.\n\n##### Validator Operators\n\nYou can compose several validators using some validator operators:\n\n* `either(arrayOfValidators)`: Performs a logical **OR** operation on an array of validators.\n* `optional(validatorOrArrayOfValidators)`: If a value is is not undefined, ensures that it satisfies the specified validators (can be a single validator or an array of validators).\n\n##### Custom Failed Validation Message\n\nYou can pass an additional parameter to all the built-in validators builder to customize the message of the error that is thrown in case of failed validation.\n\n**Example:**\n\n```\nmaxLength(16, 'A username cannot exceed 16 characters');\n```\n\n#### Custom Validation Functions\n\nIn addition to the [built-in validator builders](https://layrjs.com/docs/v1/reference/validator#built-in-validator-builders), you can validate your component attributes with your own custom validation functions.\n\nA custom validation function takes a value as first parameter and returns a boolean indicating whether the validation has succeeded or not.\n\n**Example:**\n\n```\n// JS\n\nimport {Component, attribute} from '@layr/component';\n\nclass OddNumber extends Component {\n  // Ensures that the value is an odd number\n  @attribute('number', {validators: [(value) => value % 2 === 1]}) value;\n}\n```\n\n```\n// TS\n\nimport {Component, attribute} from '@layr/component';\n\nclass OddNumber extends Component {\n  // Ensures that the value is an odd number\n  @attribute('number', {validators: [(value) => value % 2 === 1]}) value!: number;\n}\n```\n\n#### Methods\n\n##### `getFunction()` <badge type=\"secondary-outline\">instance method</badge> {#get-function-instance-method}\n\nReturns the function associated to the validator.\n\n**Returns:**\n\nA function.\n\n**Example:**\n\n```\nmaxLength(8).getFunction();\n// => function (value, maxLength) { return value.length <= maxLength; }\n```\n\n##### `getName()` <badge type=\"secondary-outline\">instance method</badge> {#get-name-instance-method}\n\nReturns the name of the validator.\n\n**Returns:**\n\nA string.\n\n**Example:**\n\n```\nmaxLength(8).getName(); // => 'maxLength'\n```\n\n##### `getArguments()` <badge type=\"secondary-outline\">instance method</badge> {#get-arguments-instance-method}\n\nReturns the arguments of the validator.\n\n**Returns:**\n\nAn array of values of any type.\n\n**Example:**\n\n```\nmaxLength(8).getArguments(); // => [8]\n```\n\n##### `getMessage()` <badge type=\"secondary-outline\">instance method</badge> {#get-message-instance-method}\n\nReturns the message of the error that is thrown in case of failed validation.\n\n**Returns:**\n\nA string.\n\n**Example:**\n\n```\nmaxLength(8).getMessage(); // => 'The validator maxLength(8) failed'\n```\n\n##### `run()` <badge type=\"secondary-outline\">instance method</badge> {#run-instance-method}\n\nRuns the validator against the specified value.\n\n**Returns:**\n\n`true` if the validation has succeeded, `false` otherwise.\n\n**Example:**\n\n```\nmaxLength(8).run('1234567'); // => true\nmaxLength(8).run('12345678'); // => true\nmaxLength(8).run('123456789'); // => false\n```\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/value-type-23f3WjnZNDfFejs5ceJVcY.immutable.md",
    "content": "### ValueType <badge type=\"primary\">class</badge> {#value-type-class}\n\nA class to handle the various types of values supported by Layr.\n\n#### Usage\n\nYou shouldn't have to create a `ValueType` instance directly. Instead, when you define an attribute (using a decorator such as [`@attribute()`](https://layrjs.com/docs/v1/reference/component#attribute-decorator)), you can specify a string representing a type of value, and a `ValueType` will be automatically created for you.\n\n**Example:**\n\n```\n// JS\n\nimport {Component, attribute, validators} from '@layr/component';\n\nconst {integer, greaterThan} = validators;\n\nclass Movie extends Component {\n  // Required 'string' attribute\n  @attribute('string') title;\n\n  // Required 'number' attribute with some validators\n  @attribute('number', {validators: [integer(), greaterThan(0)]}) reference;\n\n  // Optional 'string' attribute\n  @attribute('string?') summary;\n\n  // Required 'Director' attribute\n  @attribute('Director') director;\n\n  // Required array of 'Actor' attribute with a default value\n  @attribute('Actor[]') actors = [];\n}\n```\n\n```\n// TS\n\nimport {Component, attribute, validators} from '@layr/component';\n\nconst {integer, greaterThan} = validators;\n\nclass Movie extends Component {\n  // Required 'string' attribute\n  @attribute('string') title!: string;\n\n  // Required 'number' attribute with some validators\n  @attribute('number', {validators: [integer(), greaterThan(0)]}) reference!: number;\n\n  // Optional 'string' attribute\n  @attribute('string?') summary?: string;\n\n  // Required 'Director' attribute\n  @attribute('Director') director!: Director;\n\n  // Required array of 'Actor' attribute with a default value\n  @attribute('Actor[]') actors: Actor[] = [];\n}\n```\n\nIn case you want to access the `ValueType` instances that were created under the hood, you can do the following:\n\n```\nconst movie = new Movie({ ... });\n\nlet valueType = movie.getAttribute('title').getValueType();\nvalueType.toString(); // => 'string'\n\nvalueType = movie.getAttribute('reference').getValueType();\nvalueType.toString(); // => 'number'\nvalueType.getValidators(); // => [integerValidator, greaterThanValidator]\n\nvalueType = movie.getAttribute('summary').getValueType();\nvalueType.toString(); // => 'string?'\nvalueType.isOptional(); // => true\n\nvalueType = movie.getAttribute('director').getValueType();\nvalueType.toString(); // => 'Director'\n\nvalueType = movie.getAttribute('actors').getValueType();\nvalueType.toString(); // => 'Actor[]'\nconst itemValueType = valueType.getItemType(); // => A ValueType representing the type of the items inside the array\nitemValueType.toString(); // => Actor\n```\n\n#### Supported Types\n\nLayr supports a number of types that can be represented by a string in a way that is very similar to the way you specify basic types in [TypeScript](https://www.typescriptlang.org/).\n\n##### Scalars\n\nTo specify a scalar type, simply specify a string representing it:\n\n* `'boolean'`: A boolean.\n* `'number'`: A floating-point number.\n* `'string'`: A string.\n\n##### Arrays\n\nTo specify an array type, add `'[]'` after any other type:\n\n* `'number[]'`: An array of numbers.\n* `'string[]'`: An array of strings.\n* `'Actor[]'`: An array of `Actor`.\n* `'number[][]'`: A matrix of numbers.\n\n##### Objects\n\nTo specify a plain object type, just specify the string `'object'`:\n\n* `'object'`: A plain JavaScript object.\n\nSome common JavaScript objects are supported as well:\n\n* `'Date'`: A JavaScript `Date` instance.\n* `'RegExp'`: A JavaScript `RegExp` instance.\n\n##### Components\n\nAn attribute can hold a reference to a [`Component`](https://layrjs.com/docs/v1/reference/component) instance, or contain an [`EmbeddedComponent`](https://layrjs.com/docs/v1/reference/embedded-component) instance. To specify such a type, just specify the name of the component:\n\n* `'Director'`: A reference to a `Director` component instance.\n* `'MovieDetails'`: A `MovieDetails` embedded component instance.\n\nIt is also possible to specify a type that represents a reference to a [`Component`](https://layrjs.com/docs/v1/reference/component) class. To do so, add `'typeof '` before the name of the component:\n\n* `'typeof Director'`: A reference to the `Director` component class.\n\n##### `'?'` Modifier\n\nBy default, all attribute values are required, which means a value cannot be `undefined`. To make a value optional, add a question mark (`'?'`) after its type:\n\n* `'string?'`: A string or `undefined`.\n* `'number[]?'`: A number array or `undefined`.\n* `'number?[]'`: An array containing some values of type number or `undefined`.\n* `'Director?'`: A reference to a `Director` component instance or `undefined`.\n\n##### '`any`' Type\n\nIn some rare occasions, you may want to define an attribute that can handle any type of values. To do so, you can specify the string `'any'`:\n\n* `'any'`: Any type of values.\n\n#### Methods\n\n##### `isOptional()` <badge type=\"secondary-outline\">instance method</badge> {#is-optional-instance-method}\n\nReturns whether the value type is marked as optional. A value of a type marked as optional can be `undefined`.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nmovie.getAttribute('summary').getValueType().isOptional(); // => true\nmovie.summary = undefined; // Okay\n\nmovie.getAttribute('title').getValueType().isOptional(); // => false\nmovie.title = undefined; // Error\n```\n\n##### `getValidators()` <badge type=\"secondary-outline\">instance method</badge> {#get-validators-instance-method}\n\nReturns the validators associated to the value type.\n\n**Returns:**\n\nA array of [`Validator`](https://layrjs.com/docs/v1/reference/component).\n\n**Example:**\n\n```\nmovie.getAttribute('reference').getValueType().getValidators();\n// => [integerValidator, greaterThanValidator]\n```\n\n##### `getItemType()` <badge type=\"secondary-outline\">instance method</badge> {#get-item-type-instance-method}\n\nIn case the value type is an array, returns the value type of the items it contains.\n\n**Returns:**\n\nA [ValueType](https://layrjs.com/docs/v1/reference/value-type).\n\n**Example:**\n\n```\nmovie.getAttribute('actors').getValueType().getItemType().toString(); // => 'Actor'\n```\n\n##### `toString()` <badge type=\"secondary-outline\">instance method</badge> {#to-string-instance-method}\n\nReturns a string representation of the value type.\n\n**Returns:**\n\nA string.\n\n**Example:**\n\n```\nmovie.getAttribute('title').getValueType().toString(); // => 'string'\nmovie.getAttribute('summary').getValueType().toString(); // => 'string?'\nmovie.getAttribute('actors').getValueType().toString(); // => 'Actor[]'\n```\n"
  },
  {
    "path": "website/frontend/public/docs/v1/reference/with-roles-2PYi0037F3Mxg80b5YKb26.immutable.md",
    "content": "### WithRoles() <badge type=\"primary\">mixin</badge> {#with-roles-mixin}\n\nExtends a [`Component`](https://layrjs.com/docs/v1/reference/component) class with the ability to handle [roles](https://layrjs.com/docs/v1/reference/role).\n\n#### Usage\n\nCall `WithRoles()` with a [`Component`](https://layrjs.com/docs/v1/reference/component) class to construct a [`ComponentWithRoles`](https://layrjs.com/docs/v1/reference/with-roles#component-with-roles-class) class. Next, use the [`@role()`](https://layrjs.com/docs/v1/reference/with-roles#role-decorator) decorator to define some [roles](https://layrjs.com/docs/v1/reference/role). Lastly, use the [`@expose()`](https://layrjs.com/docs/v1/reference/component#expose-decorator) decorator to authorize some attributes or methods according to these roles.\n\n**Example:**\n\n```\nimport {Component, attribute, method, expose} from '@layr/component';\nimport {WithRoles, role} from '@layr/with-roles';\n\nclass Article extends WithRoles(Component) {\n  @role('admin') static adminRoleResolver() {\n    // ...\n  }\n\n  // Only readable by an administrator\n  @expose({get: 'admin'}) @attribute('number') viewCount = 0;\n\n  // Only callable by an administrator\n  @expose({call: 'admin'}) @method() unpublish() {\n    // ...\n  }\n}\n```\n\nOnce you have a [`ComponentWithRoles`](https://layrjs.com/docs/v1/reference/with-roles#component-with-roles-class), you can use any method provided by the `WithRoles()` mixin. For example, you can use the [`resolveRole()`](https://layrjs.com/docs/v1/reference/with-roles#resolve-role-dual-method) method to determine if the user has a specific role:\n\n```\nArticle.resolveRole('admin'); // `true` or `false` depending on the current user\n```\n\nSee the [\"Handling Authorization\"](https://layrjs.com/docs/v1/introduction/authorization) guide for a comprehensive example using the `WithRoles()` mixin.\n\n### ComponentWithRoles <badge type=\"primary\">class</badge> {#component-with-roles-class}\n\n*Inherits from [`Component`](https://layrjs.com/docs/v1/reference/component).*\n\nA `ComponentWithRoles` class is constructed by calling the `WithRoles()` mixin ([see above](https://layrjs.com/docs/v1/reference/with-roles#with-roles-mixin)).\n\n#### Roles\n\n##### `getRole(name, [options])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#get-role-dual-method}\n\nGets a role. If there is no role with the specified name, an error is thrown.\n\n**Parameters:**\n\n* `name`: The name of the role to get.\n* `options`:\n  * `fallbackToClass`: A boolean specifying whether the role should be get from the component class if there is no role with the specified name in the component prototype or instance (default: `false`).\n\n**Returns:**\n\nA [Role](https://layrjs.com/docs/v1/reference/role) instance.\n\n**Example:**\n\n```\nArticle.getRole('admin'); // => 'admin' role\n\nconst article = new Article();\narticle.getRole('admin'); // Error\narticle.getRole('admin', {fallbackToClass: true}); // => 'admin' role\n```\n\n##### `hasRole(name, [options])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#has-role-dual-method}\n\nReturns whether the [`ComponentWithRoles`](https://layrjs.com/docs/v1/reference/with-roles#component-with-roles-class) has a role with the specified name.\n\n**Parameters:**\n\n* `name`: The name of the role to check.\n* `options`:\n  * `fallbackToClass`: A boolean specifying whether the component class should be considered if there is no role with the specified name in the component prototype or instance (default: `false`).\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nArticle.hasRole('admin'); // => true\n\nconst article = new Article();\narticle.hasRole('admin'); // => false\narticle.hasRole('admin', {fallbackToClass: true}); // => true\n```\n\n##### `setRole(name, resolver)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#set-role-dual-method}\n\nSets a role in the current [`ComponentWithRoles`](https://layrjs.com/docs/v1/reference/with-roles#component-with-roles-class) class or prototype.\n\nTypically, instead of using this method, you would rather use the [`@role()`](https://layrjs.com/docs/v1/reference/with-roles#role-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the role.\n* `resolver`: A function that should return a boolean indicating whether a user has the corresponding role. The function can be asynchronous and is called with the role's parent as `this` context.\n\n**Returns:**\n\nThe [Role](https://layrjs.com/docs/v1/reference/role) instance that was created.\n\n**Example:**\n\n```\nArticle.setRole('admin', function () {\n // ...\n});\n```\n\n##### `resolveRole(name)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">possibly async</badge> {#resolve-role-dual-method}\n\nResolves a role by calling its resolver function. If there is no role with the specified name in the [`ComponentWithRoles`](https://layrjs.com/docs/v1/reference/with-roles#component-with-roles-class) class or prototype, an error is thrown.\n\nThe resolver function is called with the role's parent as `this` context.\n\nOnce a role has been resolved, the result is cached, so the resolver function is called only one time.\n\n**Parameters:**\n\n* `name`: The name of the role to resolve.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nArticle.resolveRole('admin'); // `true` or `false` depending on the current user\n```\n\n#### Decorators\n\n##### `@role(name)` <badge type=\"tertiary\">decorator</badge> {#role-decorator}\n\nDefines a [role](https://layrjs.com/docs/v1/reference/role) in a [`ComponentWithRoles`](https://layrjs.com/docs/v1/reference/with-roles#component-with-roles-class) class or prototype.\n\nThis decorator should be used to decorate a class or instance method that implements the role's resolver. The method can be asynchronous and should return a boolean indicating whether a user has the corresponding role.\n\n**Parameters:**\n\n* `name`: The name of the role to define.\n\n**Example:**\n\nSee an example of use in the [`WithRoles()`](https://layrjs.com/docs/v1/reference/with-roles#with-roles-mixin) mixin.\n#### Utilities\n\n##### `isComponentWithRolesClassOrInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-component-with-roles-class-or-instance-function}\n\nReturns whether the specified value is a [`ComponentWithRoles`](https://layrjs.com/docs/v1/reference/with-roles#component-with-roles-class) class or instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `assertIsComponentWithRolesClassOrInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#assert-is-component-with-roles-class-or-instance-function}\n\nThrows an error if the specified value is not a [`ComponentWithRoles`](https://layrjs.com/docs/v1/reference/with-roles#component-with-roles-class) class or instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/concepts/coming-soon-38q4cuyCWekHuz36oHyMKG.immutable.md",
    "content": "### Concepts\n\n#### Coming Soon\n\nThis section will introduce the basic concepts of Layr, such as:\n\n- Components\n- Controlled attributes and methods\n- Cross-layer inheritance\n- Storage\n- Routes and wrappers\n\nUnfortunately, writing documentation takes a lot of time, and the Layr project is currently handled by a [single person](https://mvila.me) who has to work for customers to make a living and is also starting a new [ambitious project](https://1place.app), which will be based on Layr.\n\nMore documentation will come for sure, but please be patient.\n\nIf you cannot wait and feel adventurous, you can figure out how to build an app with Layr by checking out:\n\n- The [\"Hello, World!\"](https://layrjs.com/docs/v2/introduction/hello-world) introductory app.\n- The pretty exhaustive [\"Reference\"](https://layrjs.com/docs/v2/reference) section.\n- Some [examples](https://layrjs.com/docs/v2/introduction/introduction#examples) of simple full-stack apps built with Layr.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/guides/coming-soon-6TWZbDxh3EVysir57k1tga.immutable.md",
    "content": "### Guides\n\n#### Coming Soon\n\nThis section will contain some guides explaining how to achieve the most common tasks and features with Layr, such as:\n\n- Local development\n- Layouts and pages\n- Data fetching and storage\n- Authentication\n- Testing\n- Deployment\n\nUnfortunately, writing documentation takes a lot of time, and the Layr project is currently handled by a [single person](https://mvila.me) who has to work for customers to make a living and is also starting a new [ambitious project](https://1place.app), which will be based on Layr.\n\nMore documentation will come for sure, but please be patient.\n\nIf you cannot wait and feel adventurous, you can figure out how to build an app with Layr by checking out:\n\n- The [\"Hello, World!\"](https://layrjs.com/docs/v2/introduction/hello-world) introductory app.\n- The pretty exhaustive [\"Reference\"](https://layrjs.com/docs/v2/reference) section.\n- Some [examples](https://layrjs.com/docs/v2/introduction/introduction#examples) of simple full-stack apps built with Layr.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/introduction/handling-authorization-1sLAebuD81FLlSvJbAuJmZ.immutable.md",
    "content": "### Handling Authorization\n\n#### Coming Soon\n\nThis tutorial will expand the [\"Hello, World!\"](https://layrjs.com/docs/v2/introduction/hello-world) app to show you how Layr handles user authorization.\n\nUnfortunately, writing tutorials takes a lot of time, and the Layr project is currently handled by a [single person](https://mvila.me) who has to work for customers to make a living and is also starting a new [ambitious project](https://1place.app), which will be based on Layr.\n\nThis tutorial will come for sure, but please be patient.\n\nIf you cannot wait and feel adventurous, you can figure out how to handle authorization with Layr by checking out:\n\n- The [`WithRoles()`](https://layrjs.com/docs/v2/reference/with-roles) mixin in the \"Reference\" section.\n- Some examples of simple full-stack apps handling authorization with Layr:\n  - [Layr Website (TS)](https://github.com/layrjs/layr/tree/master/website)\n  - [CodebaseShow (TS)](https://github.com/codebaseshow/codebaseshow)\n  - [RealWorld Example App (JS)](https://github.com/layrjs/react-layr-realworld-example-app)\n"
  },
  {
    "path": "website/frontend/public/docs/v2/introduction/hello-world/hello-world-2FHBCgFRhZX9qH9O1RkTlZ.immutable.md",
    "content": "### Hello, World!\n\nLet's start our journey into Layr by implementing the mandatory [\"Hello, World!\"](https://en.wikipedia.org/wiki/%22Hello,_World!%22_program) app, and let's make it object-oriented and full-stack!\n\n> **Note**: Layr supports both [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript) and [TypeScript](https://www.typescriptlang.org/). To select your language of choice, use the drop-down menu on the left.\n\n> **TLDR**: The completed app is available in the <!-- <if language=\"js\"> -->[Layr repository](https://github.com/layrjs/layr/tree/master/examples/v2/hello-world-js)<!-- </if> --><!-- <if language=\"ts\"> -->[Layr repository](https://github.com/layrjs/layr/tree/master/examples/v2/hello-world-ts)<!-- </if> -->.\n\n#### Prerequisites\n\n- You use a Mac with a recent version of macOS. Everything should work fine on Linux, but we haven't tested it yet. It may work on Windows, but we haven't tested it and don't plan to provide support for now.\n- You have [Node.js](https://nodejs.org/) v16 or newer installed.\n- You are familiar with [React](https://reactjs.org/), which is used in this tutorial.\n\n#### Creating the App\n\nTo make things easier, we'll use [Boostr](https://boostr.dev) to create the app and manage the local development environment.\n\nBoostr is a companion tool for Layr that takes care of everything you need to build and deploy a Layr app so you can focus on what really matters — the app's code.\n\nSo, first of all, we have to install Boostr. Run the following command in your terminal:\n\n```sh\nnpm install --global boostr\n```\n\n> **Notes**:\n>\n> - Depending on your Node.js installation, you may have to prefix the command with `sudo` so the package can be installed globally.\n> - Installing an NPM package globally is usually not recommended. But it's not a problem in this case because each app managed by Boostr uses a local Boostr package which is automatically installed. So the global Boostr package can be seen as a shortcut to the local Boostr packages installed in your apps, and, therefore, you can have different apps using different versions of Boostr.\n\nThen, run the following commands to create a directory for the app and initialize it:\n\n<!-- <if language=\"js\"> -->\n\n```sh\nmkdir hello-world\ncd hello-world\nboostr initialize @boostr/web-app-js\n```\n\n<!-- </if> -->\n\n<!-- <if language=\"ts\"> -->\n\n```sh\nmkdir hello-world\ncd hello-world\nboostr initialize @boostr/web-app-ts\n```\n\n<!-- </if> -->\n\nFinally, you can open the `hello-world` directory in your favorite [IDE](https://en.wikipedia.org/wiki/Integrated_development_environment) to explore the initial codebase.\n\n> **Note**: You can use any IDE you want, but if you use [Visual Studio Code](https://code.visualstudio.com/), you can profit from the VS Code configuration included in the [Boostr app templates](https://boostr.dev/docs#boostr-initialize-template-options). Otherwise, you may have to set up your IDE to get a suitable configuration.\n\nWe will not detail all directories and files created by Boostr because it would be out of the scope of this tutorial. If you are the kind of person who needs to understand everything, please check out the [Boostr documentation](https://boostr.dev/docs) to find out more.\n\nSo, we will only focus on two files:\n\n<!-- <if language=\"js\"> -->\n\n- `frontend/src/components/application.jsx`: The root [component](https://layrjs.com/docs/v2/reference/component) of the app frontend.\n- `backend/src/components/application.js`: The root [component](https://layrjs.com/docs/v2/reference/component) of the app backend.\n\n<!-- </if> -->\n\n<!-- <if language=\"ts\"> -->\n\n- `frontend/src/components/application.tsx`: The root [component](https://layrjs.com/docs/v2/reference/component) of the app frontend.\n- `backend/src/components/application.ts`: The root [component](https://layrjs.com/docs/v2/reference/component) of the app backend.\n\n<!-- </if> -->\n\n#### Starting the App\n\nStart the app in development mode with the following command:\n\n```sh\nboostr start\n```\n\nThe terminal should output something like this:\n\n```txt\n[database] MongoDB server started at mongodb://localhost:18160/\n[backend] Build succeeded (bundle size: 2.06MB)\n[backend] Component HTTP server started at http://localhost:18159/\n[frontend] Build succeeded (bundle size: 1.34MB)\n[frontend] Single-page application server started at http://localhost:18158/\n```\n\n> **Notes**:\n>\n> - The TCP ports used for each [local development URL](https://boostr.dev/docs#local-development-urls) were randomly set when the Boostr `initialize` command was executed to create the app in the [previous section](https://layrjs.com/docs/v2/introduction/hello-world#creating-the-app). So, it's normal if the TCP ports are different for you.\n> - Don't be freaked out by the size of the generated bundles in development mode. When you deploy your apps, the generated bundles are a lot smaller.\n\nThe last line in the terminal output should include an URL you can open in a browser to display the app.\n\nAt this point, you should see something like this in your browser:\n\n<p>\n\t<img src=\"https://layrjs.com/docs/v2/introduction/hello-world/assets/screenshot-001.immutable.png\" alt=\"Screenshot of the app initialized by Boostr\" style=\"width: 100%; margin-top: .5rem\">\n</p>\n\n#### Taking a Look at the Initial App\n\nLet's see what we have for now.\n\nWhen we bootstrapped the app with the Boostr `initialize` command, we got a minimal app which does one thing: displaying the result of a backend method in the frontend.\n\n##### Frontend\n\nHere's what the initial frontend root [component](https://layrjs.com/docs/v2/reference/component) (`Application`) looks like:\n\n```js\n// JS\n\n// frontend/src/components/application.jsx\n\nimport {Routable} from '@layr/routable';\nimport React from 'react';\nimport {layout, page, useData} from '@layr/react-integration';\n\nexport const extendApplication = (Base) => {\n  class Application extends Routable(Base) {\n    @layout('/') static MainLayout({children}) {\n      return (\n        <>\n          <this.MainPage.Link>\n            <h1>{process.env.APPLICATION_NAME}</h1>\n          </this.MainPage.Link>\n          {children()}\n        </>\n      );\n    }\n\n    @page('[/]') static MainPage() {\n      return useData(\n        async () => await this.isHealthy(),\n\n        (isHealthy) => <p>The app is {isHealthy ? 'healthy' : 'unhealthy'}.</p>\n      );\n    }\n\n    @page('[/]*') static NotFoundPage() {\n      return (\n        <>\n          <h2>Page not found</h2>\n          <p>Sorry, there is nothing here.</p>\n        </>\n      );\n    }\n  }\n\n  return Application;\n};\n```\n\n```ts\n// TS\n\n// frontend/src/components/application.tsx\n\nimport {Routable} from '@layr/routable';\nimport React, {Fragment} from 'react';\nimport {layout, page, useData} from '@layr/react-integration';\n\nimport type {Application as BackendApplication} from '../../../backend/src/components/application';\n\nexport const extendApplication = (Base: typeof BackendApplication) => {\n  class Application extends Routable(Base) {\n    declare ['constructor']: typeof Application;\n\n    @layout('/') static MainLayout({children}: {children: () => any}) {\n      return (\n        <>\n          <this.MainPage.Link>\n            <h1>{process.env.APPLICATION_NAME}</h1>\n          </this.MainPage.Link>\n          {children()}\n        </>\n      );\n    }\n\n    @page('[/]') static MainPage() {\n      return useData(\n        async () => await this.isHealthy(),\n\n        (isHealthy) => <p>The app is {isHealthy ? 'healthy' : 'unhealthy'}.</p>\n      );\n    }\n\n    @page('[/]*') static NotFoundPage() {\n      return (\n        <>\n          <h2>Page not found</h2>\n          <p>Sorry, there is nothing here.</p>\n        </>\n      );\n    }\n  }\n\n  return Application;\n};\n\nexport declare const Application: ReturnType<typeof extendApplication>;\n\nexport type Application = InstanceType<typeof Application>;\n```\n\nIf we put aside the [mixin mechanism](https://www.typescriptlang.org/docs/handbook/mixins.html) that allows the frontend `Application` component to \"inherit\" from the backend `Application` component, we can see three class methods.\n\n`MainLayout()` implements a layout for all the pages of the app:\n\n- It is decorated with [`@layout('/')`](https://layrjs.com/docs/v2/reference/react-integration#layout-decorator), which makes the method acts as a layout and defines an URL path (`'/'`) for it.\n- It renders the name of the app (using the `APPLICATION_NAME` [environment variable](https://github.com/boostrjs/boostr#environment-variables)) in an `<h1>` HTML tag, which is nested into a link pointing to the app's main page (using the [`<this.MainPage.Link>`](https://layrjs.com/docs/v2/reference/routable#route-decorator) React element).\n- It calls the `children` prop to render the content of the pages using this layout.\n\n`MainPage()` implements the main page of the app:\n\n- It is decorated with [`@page('[/]')`](https://layrjs.com/docs/v2/reference/react-integration#page-decorator), which makes the method acts as a page associated with the `'/'` URL path and specifies that the main layout (`'/'` enclosed in square brackets `'[]'`) should be used.\n- It returns the result of the [`useData()`](https://layrjs.com/docs/v2/reference/react-integration#use-data-react-hook) React hook, which calls a backend method (with `await this.isHealthy()`) and renders its result (with `<p>The app is {isHealthy ? 'healthy' : 'unhealthy'}.</p>`).\n\n`NotFoundPage()` implements a page that is displayed when a user goes to an URL that is not handled by the app:\n\n- It is decorated with [`@page('[/]*')`](https://layrjs.com/docs/v2/reference/react-integration#page-decorator), which makes the method acts as a page associated with any unhandled URL path starting with `'/'` and specifies that the main layout (`'/'` enclosed in square brackets `'[]'`) should be used.\n- It renders some simple HTML tags and texts indicating that the page cannot be found.\n\n##### Backend\n\nHere's what the initial backend root [component](https://layrjs.com/docs/v2/reference/component) (`Application`) looks like:\n\n```js\n// JS\n\n// backend/src/components/application.js\n\nimport {Component, method, expose} from '@layr/component';\n\nexport class Application extends Component {\n  @expose({call: true}) @method() static async isHealthy() {\n    return true;\n  }\n}\n```\n\n```ts\n// TS\n\n// backend/src/components/application.ts\n\nimport {Component, method, expose} from '@layr/component';\n\nexport class Application extends Component {\n  @expose({call: true}) @method() static async isHealthy() {\n    return true;\n  }\n}\n```\n\nThis component is straightforward.\n\n`isHealthy()` implements a class method that the frontend can call to check whether the app is healthy:\n\n- It is decorated with [`@expose({call: true})`](https://layrjs.com/docs/v2/reference/component#expose-decorator), which exposes the method to the frontend.\n- It is also decorated with [`@method()`](https://layrjs.com/docs/v2/reference/component#method-decorator), which is required so that the `@expose()` decorator can be used.\n- It always returns `true`, which is rather pointless. An actual `isHealthy()` method would, for example, check whether a database responds correctly.\n\n#### Displaying \"Hello, World!\" in the Frontend\n\nIt's about time to write some code.\n\nWe'll start gently by modifying the frontend to display \"Hello, World!\" instead of \"The app is healthy.\".\n\nModify the `MainPage()` class method in the <!-- <if language=\"js\"> -->`frontend/src/components/application.jsx`<!-- </if> --><!-- <if language=\"ts\"> -->`frontend/src/components/application.tsx`<!-- </if> --> file as follows:\n\n```ts\n@page('[/]') static MainPage() {\n  return <p>Hello, World!</p>;\n}\n```\n\nSave the file, and your browser should automatically refresh the page with the following contents:\n\n<p>\n\t<img src=\"https://layrjs.com/docs/v2/introduction/hello-world/assets/screenshot-002.immutable.png\" alt=\"Screenshot of the app displaying 'Hello, World!'\" style=\"width: 100%; margin-top: .5rem\">\n</p>\n\n#### Adding a Page in the Frontend\n\nSo the frontend is now displaying \"Hello, World!\" and we could call it a day.\n\nBut that was a bit too easy, don't you think?\n\nLet's spice this tutorial a little by adding a page in charge of displaying the \"Hello, World!\" message.\n\nAdd the following class method in the <!-- <if language=\"js\"> -->`frontend/src/components/application.jsx`<!-- </if> --><!-- <if language=\"ts\"> -->`frontend/src/components/application.tsx`<!-- </if> --> file:\n\n```ts\n@page('[/]hello-world') static HelloWorldPage() {\n  return <p>Hello, World!</p>;\n}\n```\n\nThat's it. The app just got a new page. You could view it by changing the URL path to `/hello-world` in your browser, but adding a link to the new page inside the main page would be better in terms of user experience.\n\nLet's do so by modifying the `MainPage()` class method as follows:\n\n```ts\n@page('[/]') static MainPage() {\n  return (\n    <p>\n      <this.HelloWorldPage.Link>\n        See the \"Hello, World!\" page\n      </this.HelloWorldPage.Link>\n    </p>\n  );\n}\n```\n\nHold on. What's going on here? Is it how we create links with Layr? Where is the URL path (`'/hello-world'`) of the \"Hello, World!\" page? Well, in a Layr app, except in the `@page()` decorators, you should never encounter any URL path.\n\nWe hope it doesn't sound too magical because it is not. Any method decorated with [`@page()`](https://layrjs.com/docs/v2/reference/react-integration#page-decorator) automatically gets some attached [shortcut functions](https://layrjs.com/docs/v2/reference/routable#route-decorator), such as `Link()`, which implements a React component rendering a `<a>` tag referencing the URL path of the page.\n\nYour browser should now display the following page:\n\n<p>\n\t<img src=\"https://layrjs.com/docs/v2/introduction/hello-world/assets/screenshot-003.immutable.png\" alt=\"Screenshot of the app displaying a link to the 'Hello, World!' page\" style=\"width: 100%; margin-top: .5rem\">\n</p>\n\n#### Getting the \"Hello, World\" Message from the Backend\n\nAt the beginning of this tutorial, we promised to create a full-stack app, but currently, the backend is not involved, and everything happens in the frontend.\n\nLet's fix that by moving the \"business logic\" generating the \"Hello, World!\" message to the backend.\n\nFirst, add the following class method in the <!-- <if language=\"js\"> -->`backend/src/components/application.js`<!-- </if> --><!-- <if language=\"ts\"> -->`backend/src/components/application.ts`<!-- </if> --> file:\n\n```ts\n@expose({call: true}) @method() static async getHelloWorld() {\n  return 'Hello, World!';\n}\n```\n\nThis method will be callable from the frontend (thanks to the [`@expose()`](https://layrjs.com/docs/v2/reference/component#expose-decorator) and [`@method()`](https://layrjs.com/docs/v2/reference/component#method-decorator) decorators) and will return the `'Hello, World!'` string.\n\n> **Note**: Since the `isHealthy()` class method is not used anymore, you can remove it if you want.\n\nSave the file to restart the backend, and then modify the `HelloWorldPage()` class method in the <!-- <if language=\"js\"> -->`frontend/src/components/application.jsx`<!-- </if> --><!-- <if language=\"ts\"> -->`frontend/src/components/application.tsx`<!-- </if> --> file as follows:\n\n```ts\n@page('[/]hello-world') static HelloWorldPage() {\n  return useData(\n    async () => await this.getHelloWorld(),\n\n    (helloWorld) => <p>{helloWorld}</p>\n  );\n}\n```\n\nAs seen in the initial [`MainPage()`](https://layrjs.com/docs/v2/introduction/hello-world#frontend) method, we use the [`useData()`](https://layrjs.com/docs/v2/reference/react-integration#use-data-react-hook) React hook to call a backend method (with `await this.getHelloWorld()`) and render its result (with `<p>{helloWorld}</p>`).\n\nIf you refresh the \"Hello, World!\" page in your browser, you should not see any difference. However, if you inspect the network requests via the browser's developer tools, you should see that the `'Hello, World'` string displayed in the frontend comes from the backend.\n\n#### One Last Thing\n\nTechnically, we now have a full-stack app involving a frontend and a backend.\n\nBut the app is a bit static. The backend always returns the same `'Hello, World'` string, which is rather boring.\n\nLet's make the app more dynamic by modifying the `getHelloWorld()` class method in the <!-- <if language=\"js\"> -->`backend/src/components/application.js`<!-- </if> --><!-- <if language=\"ts\"> -->`backend/src/components/application.ts`<!-- </if> --> file as follows:\n\n```ts\n@expose({call: true}) @method() static async getHelloWorld() {\n  const translations = ['Hello, World!', 'Bonjour le monde !', 'こんにちは世界！'];\n\n  const translation = translations[Math.round(Math.random() * (translations.length - 1))];\n\n  return translation;\n}\n```\n\nSave the file to restart the backend, and now, when you refresh the \"Hello, World!\" page several times in your browser, you should see some translations randomly picked up.\n\nHere's an example showing the Japanese translation:\n\n<p>\n\t<img src=\"https://layrjs.com/docs/v2/introduction/hello-world/assets/screenshot-004.immutable.png\" alt=\"Screenshot of the app displaying 'Hello, World!' in Japanese\" style=\"width: 100%; margin-top: .5rem\">\n</p>\n\nWe hardcoded the translations in the `getHelloWorld()` class method, which is OK for this tutorial. But in a real-world app, storing the translations in a database would be better so an admin can edit them. We'll see how to achieve that in [another tutorial](https://layrjs.com/docs/v2/introduction/storing-data) that remains to be written.\n\n#### Wrapping Up\n\nIn this first tutorial, we saw how to build a basic full-stack app with Layr:\n\n- We [created an app](https://layrjs.com/docs/v2/introduction/hello-world#creating-the-app) and [started it](https://layrjs.com/docs/v2/introduction/hello-world#starting-the-app) in development mode with a few [Boostr](https://boostr.dev) commands.\n- We discovered how to implement [layouts](https://layrjs.com/docs/v2/introduction/hello-world#taking-a-look-at-the-initial-app), [pages](https://layrjs.com/docs/v2/introduction/hello-world#adding-a-page-in-the-frontend), and [links](https://layrjs.com/docs/v2/introduction/hello-world#adding-a-page-in-the-frontend) encapsulated in a Layr [component](https://layrjs.com/docs/v2/reference/component).\n- We found out how [a backend method could be called from the frontend](https://layrjs.com/docs/v2/introduction/hello-world#getting-the-hello-world-message-from-the-backend) thanks to the cross-layer class inheritance ability of Layr.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/introduction/introduction-1iLUBREWe2BB65qw9G4uzm.immutable.md",
    "content": "### Introduction\n\n> **Note**: Layr v2 is published on NPM, but the documentation is still a work in progress.\n\n#### Overview\n\nLayr is a set of JavaScript/TypeScript libraries that dramatically simplify the development of highly dynamic full-stack apps.\n\nTypically, a highly dynamic full-stack app comprises a frontend and a backend running in two different environments connected through a web API (REST, GraphQL, etc.)\n\nSeparating the frontend and the backend is a good thing. Still, the problem is that building a web API usually leads to a lot of code scattering, knowledge duplication, boilerplate, and accidental complexity.\n\nLayr removes the need to build a web API and [reunites the frontend and backend](https://dev.to/mvila/good-bye-web-apis-2bel) so that you can experience them as a single entity.\n\nOn the frontend side, Layr gives you [routing capabilities](https://layrjs.com/docs/v2/reference/routable) and [object observability](https://layrjs.com/docs/v2/reference/observable) so that you don't need to add an external router or a state manager.\n\nOn the backend side, Layr offers an [ORM](https://layrjs.com/docs/v2/reference/storable) (which can be exposed to the frontend) to make data storage as easy as possible.\n\n#### Made for Highly Dynamic Apps\n\nLayr stands out for building apps that offer rich user interfaces, such as the good old desktop apps.\n\nEven if they both run in a browser, we should clearly differentiate \"websites\" and \"web apps\".\n\n##### Websites\n\nWebsites provide fast load time, good [SEO](https://en.wikipedia.org/wiki/Search_engine_optimization), and a few dynamic parts.\n\nSome examples fitting in this category are e-commerce websites (e.g., [Amazon](https://www.amazon.com/)), marketplace platforms (e.g., [Airbnb](https://www.airbnb.com/)), or online newspapers (e.g., [The New York Times](https://www.nytimes.com/)).\n\nLayr is inappropriate for building these kinds of websites because it relies on a [Single-page application](https://en.wikipedia.org/wiki/Single-page_application) architecture and doesn't provide server-side rendering.\n\nSo, instead of Layr, you should use some frameworks such as [Next.js](https://nextjs.org/), [Nuxt.js](https://nextjs.org/), or [Remix](https://remix.run/).\n\n##### Web Apps\n\nWeb apps provide rich user interfaces and are all made of dynamic parts.\n\nSome examples fitting in this category are productivity apps (e.g., [Notion](https://www.notion.so/)), communication apps (e.g., [Slack](https://slack.com/)), or design apps (e.g., [Figma](https://www.figma.com/)).\n\nLayr is made for building these kinds of apps with a straightforward architecture:\n\n- The frontend exposes an interface for _humans_.\n- The backend exposes an interface for _computers_.\n\nNote that the frontend can obviously run in a browser, but it can also run on mobile and desktop with the help of frameworks such as [React Native](https://reactnative.dev/) or [Electron](https://www.electronjs.org/).\n\n#### Core Features\n\nLayr provides everything you need to build an app from start to finish:\n\n- **Cross-layer inheritance**: A frontend class can \"inherit\" from a backend class, so some exposed [attributes](https://layrjs.com/docs/v2/reference/attribute) are automatically transported between the frontend and the backend. Also, you can call some exposed backend [methods](https://layrjs.com/docs/v2/reference/method) directly from the frontend.\n- **Controlled attributes**: An attribute can be [type-checked](https://layrjs.com/docs/v2/reference/value-type), [sanitized](https://layrjs.com/docs/v2/reference/sanitizer), [validated](https://layrjs.com/docs/v2/reference/validator), [serialized](https://layrjs.com/docs/v2/reference/component#serialization), and [observed](https://layrjs.com/docs/v2/reference/observable) at runtime.\n- **Storage**: A class instance can be [persisted](https://layrjs.com/docs/v2/reference/storable) in a database. Currently, only [MongoDB](https://www.mongodb.com/) is supported, but we plan to support more databases in the future.\n- **Routing**: A method can be [associated with an URL](https://layrjs.com/docs/v2/reference/routable) and controlled by a [navigator](https://layrjs.com/docs/v2/reference/navigator) so that this method is automatically called when the user navigates.\n- **Layouts**: A method can act as a [wrapper](https://layrjs.com/docs/v2/reference/wrapper) for other methods with a shared URL path prefix. This way, you can easily create [layouts](https://layrjs.com/docs/v2/reference/react-integration#layout-decorator) for your [pages](https://layrjs.com/docs/v2/reference/react-integration#page-decorator).\n- **Authorization**: [User-role-based](https://layrjs.com/docs/v2/reference/with-roles) authorizations can be set to restrict some attributes or methods.\n- **Integrations**: Integration helpers are provided to facilitate the integration of the most popular libraries or services. Currently, two integration helpers are available: [react-integration](https://layrjs.com/docs/v2/reference/react-integration) and [aws-integration](https://layrjs.com/docs/v2/reference/aws-integration). But more should come shortly.\n- **Interoperability**: The backend is automatically exposed through a [Deepr API](https://deepr.io), so you can consume it from any frontend even though it's not built with Layr. And if you want to bring a more standard API (e.g., REST) to your backend, it's straightforward to add such an API in your Layr backend.\n\n#### Core Principles\n\nHere's a quick taste of the core principles upon which Layr is built:\n\n- **Object-oriented**: Layr embraces the object-oriented paradigm in all aspects of an app and allows you to organize your code in a way that is as cohesive as possible.\n- **End-to-end type safety**: When you use TypeScript, from the frontend to the database (which goes through the backend), every single piece of a Layr app can be type-safe.\n- **100% DRY**: A Layr app can be fully [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) (i.e., zero knowledge duplication).\n- **Low-level**: Layr is designed to be as closest to the language as possible and can be seen as a [language extension](https://layrjs.com/blog/articles/Getting-the-Right-Level-of-Generalization-7xpk37) in many ways.\n- **Unopinionated**: Layr has strong opinions about itself but doesn't force you to use any external library, service, or tool.\n\n#### Command-Line Interface\n\nLayr is just a set of low-level JavaScript/TypeScript libraries and does not come with a CLI.\n\nSo, you can use Layr with any development and deployment tools.\n\nHowever, we know that setting up a development environment and a deployment mechanism can be challenging.\n\nSo, we created [Boostr](https://boostr.dev), a companion tool that takes care of everything you need to build and deploy a Layr app.\n\nCheck out the [Boostr documentation](https://boostr.dev/docs) to find out more.\n\n#### Compatibility\n\nLayr is implemented in [TypeScript](https://www.typescriptlang.org/), but you can use either JavaScript or TypeScript to build your app.\n\nIf you are using JavaScript, you'll need to compile your code with [Babel](https://babeljs.io/) to take advantage of some novel JavaScript features such as \"decorators\".\n\nIf you are using TypeScript, all you need is the TypeScript compiler ([`tsc`](https://www.typescriptlang.org/docs/handbook/compiler-options.html)).\n\n> **Note**: If you use [Boostr](https://boostr.dev) to manage your app's development and deployment, you don't have to worry about compiling your code because it is automatically handled.\n\nTo run your app, you'll need a JavaScript runtime for both the frontend and the backend.\n\n##### Frontend\n\n###### Web\n\nAny modern browser should work fine.\n\nHere are the minimum versions with which Layr is tested:\n\n- Chrome v55\n- Safari v11\n- Firefox v54\n- Edge Chromium\n\n###### Mobile and Desktop\n\nAny mobile or desktop app framework using JavaScript or TypeScript (such as [React Native](https://reactnative.dev/) or [Electron](https://www.electronjs.org/)) should work fine.\n\n##### Backend\n\nAny environment running [Node.js](https://nodejs.org/) v16 or later is supported.\n\n#### Examples\n\nHere are some examples of simple full-stack apps that you can check out:\n\n- [CRUD Example App (JS)](https://github.com/layrjs/crud-example-app-js-boostr)\n- [CRUD Example App (TS)](https://github.com/layrjs/crud-example-app-ts-boostr)\n- [Layr Website (TS)](https://github.com/layrjs/layr/tree/master/website)\n- [CodebaseShow (TS)](https://github.com/codebaseshow/codebaseshow)\n- [RealWorld Example App (JS)](https://github.com/layrjs/react-layr-realworld-example-app)\n"
  },
  {
    "path": "website/frontend/public/docs/v2/introduction/storing-data-40jzWiVleR8ZE0fqfGTbO2.immutable.md",
    "content": "### Storing Data\n\n#### Coming Soon\n\nThis tutorial will expand the [\"Hello, World!\"](https://layrjs.com/docs/v2/introduction/hello-world) app to show you how Layr handles database storage.\n\nUnfortunately, writing tutorials takes a lot of time, and the Layr project is currently handled by a [single person](https://mvila.me) who has to work for customers to make a living and is also starting a new [ambitious project](https://1place.app), which will be based on Layr.\n\nThis tutorial will come for sure, but please be patient.\n\nIf you cannot wait and feel adventurous, you can figure out how to store data with Layr by checking out:\n\n- The [`Storable()`](https://layrjs.com/docs/v2/reference/storable) mixin in the \"Reference\" section.\n- Some [examples](https://layrjs.com/docs/v2/introduction/introduction#examples) of simple full-stack apps built with Layr.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/addressable-6n5npS1cCoPKLkFrYU0Pk7.immutable.md",
    "content": "### Addressable <badge type=\"primary\">class</badge> {#addressable-class}\n\nAn abstract class from which the classes [`Route`](https://layrjs.com/docs/v2/reference/route) and [`Wrapper`](https://layrjs.com/docs/v2/reference/wrapper) are constructed.\n\nAn addressable is composed of:\n\n- A name matching a method of a [routable component](https://layrjs.com/docs/v2/reference/routable#routable-component-class).\n- The canonical [URL pattern](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type) of the addressable.\n- Some optional [URL parameters](https://layrjs.com/docs/v2/reference/addressable#url-parameters-type) associated with the addressable.\n- Some optional [URL pattern aliases](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type) associated with the addressable.\n\n#### Usage\n\nTypically, you create a `Route` or a `Wrapper` and associate it to a routable component by using the [`@route()`](https://layrjs.com/docs/v2/reference/routable#route-decorator) or [`@wrapper()`](https://layrjs.com/docs/v2/reference/routable#wrapper-decorator) decorators.\n\nSee an example of use in the [`BrowserNavigatorView`](https://layrjs.com/docs/v2/reference/react-integration#browser-navigator-view-react-component) React component.\n\n#### Creation\n\n##### `new Addressable(name, pattern, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates an instance of [`Addressable`](https://layrjs.com/docs/v2/reference/addressable), which can represent a [`Route`](https://layrjs.com/docs/v2/reference/route) or a [`Wrapper`](https://layrjs.com/docs/v2/reference/wrapper).\n\nTypically, instead of using this constructor, you would rather use the [`@route()`](https://layrjs.com/docs/v2/reference/routable#route-decorator) or [`@wrapper()`](https://layrjs.com/docs/v2/reference/routable#wrapper-decorator) decorators.\n\n**Parameters:**\n\n* `name`: The name of the addressable.\n* `pattern`: The canonical [URL pattern](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type) of the addressable.\n* `options`:\n  * `parameters`: An optional object containing some [URL parameters](https://layrjs.com/docs/v2/reference/addressable#url-parameters-type).\n  * `aliases`: An optional array containing some [URL pattern aliases](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type).\n\n**Returns:**\n\nThe [`Addressable`](https://layrjs.com/docs/v2/reference/addressable) instance that was created.\n\n**Example:**\n\n```\nconst addressable = new Addressable('View', '/', {aliases: ['/home']});\n```\n\n#### Basic Methods\n\n##### `getName()` <badge type=\"secondary-outline\">instance method</badge> {#get-name-instance-method}\n\nReturns the name of the addressable.\n\n**Returns:**\n\nA string.\n\n**Example:**\n\n```\nconst addressable = new Addressable('View', '/');\n\naddressable.getName(); // => 'View'\n```\n\n##### `getPattern()` <badge type=\"secondary-outline\">instance method</badge> {#get-pattern-instance-method}\n\nReturns the canonical URL pattern of the addressable.\n\n**Returns:**\n\nAn [URL pattern](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type) string.\n\n**Example:**\n\n```\nconst addressable = new Addressable('View', '/movies/:slug', {aliases: ['/films/:slug']});\n\naddressable.getPattern(); // => '/movies/:slug'\n```\n\n##### `getAliases()` <badge type=\"secondary-outline\">instance method</badge> {#get-aliases-instance-method}\n\nReturns the URL pattern aliases of the addressable.\n\n**Returns:**\n\nAn array of [URL pattern](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type) strings.\n\n**Example:**\n\n```\nconst addressable = new Addressable('View', '/', {aliases: ['/home']});\n\naddressable.getAliases(); // => ['/home']\n```\n\n#### URL Matching and Generation\n\n##### `matchURL(url)` <badge type=\"secondary-outline\">instance method</badge> {#match-url-instance-method}\n\nChecks if the addressable matches the specified URL.\n\n**Parameters:**\n\n* `url`: A string or a [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) object.\n\n**Returns:**\n\nIf the addressable matches the specified URL, a plain object containing the identifiers and parameters included in the URL is returned. Otherwise, `undefined` is returned.\n\n**Example:**\n\n```\nconst addressable = new Addressable('View', '/movies/:slug', {\n params: {showDetails: 'boolean?'}\n});\n\naddressable.matchURL('/movies/abc123');\n// => {identifiers: {slug: 'abc123'}, parameters: {showDetails: undefined}}\n\naddressable.matchURL('/movies/abc123?showDetails=1');\n// => {identifiers: {slug: 'abc123'}, parameters: {showDetails: true}}\n\naddressable.matchURL('/films'); // => undefined\n```\n\n##### `generateURL([identifiers], [params], [options])` <badge type=\"secondary-outline\">instance method</badge> {#generate-url-instance-method}\n\nGenerates an URL for the addressable.\n\n**Parameters:**\n\n* `identifiers`: An optional object containing the identifiers to include in the generated URL.\n* `params`: An optional object containing the parameters to include in the generated URL.\n* `options`:\n  * `hash`: An optional string specifying a hash (i.e., a [fragment identifier](https://en.wikipedia.org/wiki/URI_fragment)) to include in the generated URL.\n\n**Returns:**\n\nA string.\n\n**Example:**\n\n```\nconst addressable = new Addressable('View', '/movies/:slug', {\n params: {showDetails: 'boolean?'}\n});\n\naddressable.generateURL({slug: 'abc123'}); // => '/movies/abc123'\n\naddressable.generateURL({slug: 'abc123'}, {showDetails: true});\n// => '/movies/abc123?showDetails=1'\n\naddressable.generateURL({slug: 'abc123'}, {showDetails: true}, {hash: 'actors'});\n// => '/movies/abc123?showDetails=1#actors'\n\naddressable.generateURL({}); // => Error (the slug parameter is mandatory)\n```\n\n#### Types\n\n##### `URLPattern` <badge type=\"primary-outline\">type</badge> {#url-pattern-type}\n\nA string defining the canonical URL pattern (or an URL pattern alias) of an addressable.\n\nAn URL pattern is composed of a *route pattern* (e.g., `'/movies'`) and can be prefixed with a *wrapper pattern*, which should be enclosed with square brackets (e.g., `'[/admin]'`).\n\n*Route patterns* and *wrapper patterns* can be composed of several *segments* separated by slashes (e.g., `'/movies/top-50'` or `'[/admin/movies]'`).\n\nA *segment* can be an arbitrary string (e.g., `'movies'`) or the name of a [component identifier attribute](https://layrjs.com/docs/v2/reference/identifier-attribute) (e.g., `'id'`) prefixed with a colon sign (`':'`). Note that a component identifier attribute can reference an identifier attribute of a related component (e.g., `'collection.id'`).\n\nOptionally, an URL pattern can be suffixed with wildcard character (`'*'`) to represent a catch-all URL.\n\n**Examples:**\n\n- `'/'`: Root URL pattern.\n- `'/movies'`: URL pattern without identifier attributes.\n- `'/movies/:id'`: URL pattern with one identifier attribute (`id`).\n- `'/collections/:collection.id/movies/:id'`: URL pattern with two identifier attributes (`collection.id` and `id`).\n- `[/]movies`: URL pattern composed of a wrapper pattern (`'[/]'`) and a route pattern (`'movies'`).\n- `'[/collections/:collection.id]/movies/:id'`: URL pattern composed of a wrapper pattern (`'[/collections/:collection.id]'`), a route pattern (`'/movies/:id'`), and two identifier attributes (`collection.id` and `id`).\n- `'/*'`: URL pattern that can match any URL. It can be helpful to display, for example, a \"Not Found\" page.\n\n##### `URLParameters` <badge type=\"primary-outline\">type</badge> {#url-parameters-type}\n\nAn object defining the URL parameters of an addressable.\n\nThe object can contain some pairs of `name` and `type` where `name` should be an arbitrary string representing the name of an URL parameter and `type` should be a string representing its type.\n\nCurrently, `type` can be one of the following strings:\n\n- `'boolean'`\n- `'number'`\n- `'string'`\n- `'Date'`\n\nOptionally, `type` can be suffixed with a question mark (`'?'`) to specify an optional URL parameter.\n\n**Examples:**\n\n- `{step: 'number'}\n- `{showDetails: 'boolean?'}`\n- `{page: 'number?', orderBy: 'string?'}`\n\n#### Utilities\n\n##### `isAddressableClass(value)` <badge type=\"tertiary-outline\">function</badge> {#is-addressable-class-function}\n\nReturns whether the specified value is an [`Addressable`](https://layrjs.com/docs/v2/reference/addressable) class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isAddressableInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-addressable-instance-function}\n\nReturns whether the specified value is an [`Addressable`](https://layrjs.com/docs/v2/reference/addressable) instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/attribute-4IRansPGIm4E3gunH3Ztlq.immutable.md",
    "content": "### Attribute <badge type=\"primary\">class</badge> {#attribute-class}\n\n*Inherits from [`Property`](https://layrjs.com/docs/v2/reference/property) and [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class).*\n\nAn `Attribute` represents an attribute of a [Component](https://layrjs.com/docs/v2/reference/component) class, prototype, or instance. It plays the role of a regular JavaScript object attribute, but brings some extra features such as runtime type checking, validation, serialization, or observability.\n\n#### Usage\n\nTypically, you create an `Attribute` and associate it to a component by using the [`@attribute()`](https://layrjs.com/docs/v2/reference/component#attribute-decorator) decorator.\n\nFor example, here is how you would define a `Movie` class with some attributes:\n\n```\n// JS\n\nimport {Component, attribute, validators} from '@layr/component';\n\nconst {minLength} = validators;\n\nclass Movie extends Component {\n  // Optional 'string' class attribute\n  @attribute('string?') static customName;\n\n  // Required 'string' instance attribute\n  @attribute('string') title;\n\n  // Optional 'string' instance attribute with a validator and a default value\n  @attribute('string?', {validators: [minLength(16)]}) summary = '';\n}\n```\n\n```\n// TS\n\nimport {Component, attribute, validators} from '@layr/component';\n\nconst {minLength} = validators;\n\nclass Movie extends Component {\n  // Optional 'string' class attribute\n  @attribute('string?') static customName?: string;\n\n  // Required 'string' instance attribute\n  @attribute('string') title!: string;\n\n  // Optional 'string' instance attribute with a validator and a default value\n  @attribute('string?', {validators: [minLength(16)]}) summary? = '';\n}\n```\n\nThen you can access the attributes like you would normally do with regular JavaScript objects:\n\n```\nMovie.customName = 'Film';\nMovie.customName; // => 'Film'\n\nconst movie = new Movie({title: 'Inception'});\nmovie.title; // => 'Inception'\nmovie.title = 'Inception 2';\nmovie.title; // => 'Inception 2'\nmovie.summary; // => '' (default value)\n```\n\nAnd you can take profit of some extra features:\n\n```\n// Runtime type checking\nmovie.title = 123; // Error\nmovie.title = undefined; // Error\n\n// Validation\nmovie.summary = undefined;\nmovie.isValid(); // => true (movie.summary is optional)\nmovie.summary = 'A nice movie.';\nmovie.isValid(); // => false (movie.summary is too short)\nmovie.summary = 'An awesome movie.'\nmovie.isValid(); // => true\n\n// Serialization\nmovie.serialize();\n// => {__component: 'Movie', title: 'Inception 2', summary: 'An awesome movie.'}\n```\n\n#### Creation\n\n##### `new Attribute(name, parent, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates an instance of [`Attribute`](https://layrjs.com/docs/v2/reference/attribute). Typically, instead of using this constructor, you would rather use the [`@attribute()`](https://layrjs.com/docs/v2/reference/component#attribute-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the attribute.\n* `parent`: The component class, prototype, or instance that owns the attribute.\n* `options`:\n  * `valueType`: A string specifying the [type of values](https://layrjs.com/docs/v2/reference/value-type#supported-types) the attribute can store (default: `'any'`).\n  * `value`: The initial value of the attribute (usable for class attributes only).\n  * `default`: The default value (or a function returning the default value) of the attribute (usable for instance attributes only).\n  * `sanitizers`: An array of [sanitizers](https://layrjs.com/docs/v2/reference/sanitizer) for the value of the attribute.\n  * `validators`: An array of [validators](https://layrjs.com/docs/v2/reference/validator) for the value of the attribute.\n  * `items`:\n    * `sanitizers`: An array of [sanitizers](https://layrjs.com/docs/v2/reference/sanitizer) for the items of an array attribute.\n    * `validators`: An array of [validators](https://layrjs.com/docs/v2/reference/validator) for the items of an array attribute.\n  * `getter`: A getter function for getting the value of the attribute. Plays the same role as a regular [JavaScript getter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get).\n  * `setter`: A setter function for setting the value of the attribute. Plays the same role as a regular [JavaScript setter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set).\n  * `exposure`: A [`PropertyExposure`](https://layrjs.com/docs/v2/reference/property#property-exposure-type) object specifying how the attribute should be exposed to remote access.\n\n**Returns:**\n\nThe [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) instance that was created.\n\n**Example:**\n\n```\nimport {Component, Attribute} from '@layr/component';\n\nclass Movie extends Component {}\n\nconst title = new Attribute('title', Movie.prototype, {valueType: 'string'});\n\ntitle.getName(); // => 'title'\ntitle.getParent(); // => Movie.prototype\ntitle.getValueType().toString(); // => 'string'\n```\n\n#### Property Methods\n\nSee the methods that are inherited from the [`Property`](https://layrjs.com/docs/v2/reference/property#basic-methods) class.\n\n#### Value Type\n\n##### `getValueType()` <badge type=\"secondary-outline\">instance method</badge> {#get-value-type-instance-method}\n\nReturns the type of values the attribute can store.\n\n**Returns:**\n\nA [ValueType](https://layrjs.com/docs/v2/reference/value-type) instance.\n\n**Example:**\n\n```\nconst title = Movie.prototype.getAttribute('title');\ntitle.getValueType(); // => A ValueType instance\ntitle.getValueType().toString(); // => 'string'\ntitle.getValueType().isOptional(); // => false\n```\n\n#### Value\n\n##### `getValue([options])` <badge type=\"secondary-outline\">instance method</badge> {#get-value-instance-method}\n\nReturns the current value of the attribute.\n\n**Parameters:**\n\n* `options`:\n  * `throwIfUnset`: A boolean specifying whether the method should throw an error if the value is not set (default: `true`). If `false` is specified and the value is not set, the method returns `undefined`.\n\n**Returns:**\n\nA value of the type handled by the attribute.\n\n**Example:**\n\n```\nconst title = movie.getAttribute('title');\ntitle.getValue(); // => 'Inception'\ntitle.unsetValue();\ntitle.getValue(); // => Error\ntitle.getValue({throwIfUnset: false}); // => undefined\n```\n\n##### `setValue(value, [options])` <badge type=\"secondary-outline\">instance method</badge> {#set-value-instance-method}\n\nSets the value of the attribute. If the type of the value doesn't match the expected type, an error is thrown.\n\nWhen the attribute's value changes, the observers of the attribute are automatically executed, and the observers of the parent component are executed as well.\n\n**Parameters:**\n\n* `value`: The value to be set.\n* `options`:\n  * `source`: A string specifying the [source of the value](https://layrjs.com/docs/v2/reference/attribute#value-source-type) (default: `'local'`).\n\n**Example:**\n\n```\nconst title = movie.getAttribute('title');\ntitle.setValue('Inception 2');\ntitle.setValue(123); // => Error\n```\n\n##### `unsetValue()` <badge type=\"secondary-outline\">instance method</badge> {#unset-value-instance-method}\n\nUnsets the value of the attribute. If the value is already unset, nothing happens.\n\n**Example:**\n\n```\nconst title = movie.getAttribute('title');\ntitle.isSet(); // => true\ntitle.unsetValue();\ntitle.isSet(); // => false\n```\n\n##### `isSet()` <badge type=\"secondary-outline\">instance method</badge> {#is-set-instance-method}\n\nReturns whether the value of the attribute is set or not.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nconst title = movie.getAttribute('title');\ntitle.isSet(); // => true\ntitle.unsetValue();\ntitle.isSet(); // => false\n```\n\n#### Value Source\n\n##### `getValueSource()` <badge type=\"secondary-outline\">instance method</badge> {#get-value-source-instance-method}\n\nReturns the source of the value of the attribute.\n\n**Returns:**\n\nA [`ValueSource`](https://layrjs.com/docs/v2/reference/attribute#value-source-type) string.\n\n**Example:**\n\n```\nconst title = movie.getAttribute('title');\ntitle.getValueSource(); // => 'local' (the value was set locally)\n```\n\n##### `setValueSource(source)` <badge type=\"secondary-outline\">instance method</badge> {#set-value-source-instance-method}\n\nSets the source of the value of the attribute.\n\n**Parameters:**\n\n* `source`: A [`ValueSource`](https://layrjs.com/docs/v2/reference/attribute#value-source-type) string.\n\n**Example:**\n\n```\nconst title = movie.getAttribute('title');\ntitle.setValueSource('local'); // The value was set locally\ntitle.setValueSource('server'); // The value came from an upper layer\ntitle.setValueSource('client'); // The value came from a lower layer\n```\n\n##### `ValueSource` <badge type=\"primary-outline\">type</badge> {#value-source-type}\n\nA string representing the source of a value.\n\nCurrently, four types of sources are supported:\n\n* `'server'`: The value comes from an upper layer.\n* `'store'`: The value comes from a store.\n* `'local'`: The value comes from the current layer.\n* `'client'`: The value comes from a lower layer.\n\n#### Default Value\n\n##### `getDefault()` <badge type=\"secondary-outline\">instance method</badge> {#get-default-instance-method}\n\nReturns the default value of the attribute as specified when the attribute was created.\n\n**Returns:**\n\nA value or a function returning a value.\n\n**Example:**\n\n```\nconst summary = movie.getAttribute('summary');\nsummary.getDefault(); // => function () { return ''; }\n```\n\n##### `evaluateDefault()` <badge type=\"secondary-outline\">instance method</badge> {#evaluate-default-instance-method}\n\nEvaluate the default value of the attribute. If the default value is a function, the function is called (with the attribute's parent as `this` context), and the result is returned. Otherwise, the default value is returned as is.\n\n**Returns:**\n\nA value of any type.\n\n**Example:**\n\n```\nconst summary = movie.getAttribute('summary');\nsummary.evaluateDefault(); // ''\n```\n\n#### Validation\n\n##### `validate([attributeSelector])` <badge type=\"secondary-outline\">instance method</badge> {#validate-instance-method}\n\nValidates the value of the attribute. If the value doesn't pass the validation, an error is thrown. The error is a JavaScript `Error` instance with a `failedValidators` custom attribute which contains the result of the [`runValidators()`](https://layrjs.com/docs/v2/reference/attribute#run-validators-instance-method) method.\n\n**Parameters:**\n\n* `attributeSelector`: In case the value of the attribute is a component, your can pass an [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the component's attributes to be validated (default: `true`, which means that all the component's attributes will be validated).\n\n**Example:**\n\n```\n// JS\n\nimport {Component, attribute, validators} from '@layr/component';\n\nconst {notEmpty} = validators;\n\nclass Movie extends Component {\n  @attribute('string', {validators: [notEmpty()]}) title;\n}\n\nconst movie = new Movie({title: 'Inception'});\nconst title = movie.getAttribute('title');\n\ntitle.getValue(); // => 'Inception'\ntitle.validate(); // All good!\ntitle.setValue('');\ntitle.validate(); // => Error {failedValidators: [{validator: ..., path: ''}]}\n```\n```\n// TS\n\nimport {Component, attribute, validators} from '@layr/component';\n\nconst {notEmpty} = validators;\n\nclass Movie extends Component {\n  @attribute('string', {validators: [notEmpty()]}) title!: string;\n}\n\nconst movie = new Movie({title: 'Inception'});\nconst title = movie.getAttribute('title');\n\ntitle.getValue(); // => 'Inception'\ntitle.validate(); // All good!\ntitle.setValue('');\ntitle.validate(); // => Error {failedValidators: [{validator: ..., path: ''}]}\n```\n\n##### `isValid([attributeSelector])` <badge type=\"secondary-outline\">instance method</badge> {#is-valid-instance-method}\n\nReturns whether the value of the attribute is valid.\n\n**Parameters:**\n\n* `attributeSelector`: In case the value of the attribute is a component, your can pass an [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the component's attributes to be validated (default: `true`, which means that all the component's attributes will be validated).\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\n// See the `title` definition in the `validate()` example\n\ntitle.getValue(); // => 'Inception'\ntitle.isValid(); // => true\ntitle.setValue('');\ntitle.isValid(); // => false\n```\n\n##### `runValidators([attributeSelector])` <badge type=\"secondary-outline\">instance method</badge> {#run-validators-instance-method}\n\nRuns the validators with the value of the attribute.\n\n**Parameters:**\n\n* `attributeSelector`: In case the value of the attribute is a component, your can pass an [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the component's attributes to be validated (default: `true`, which means that all the component's attributes will be validated).\n\n**Returns:**\n\nAn array containing the validators that have failed. Each item is a plain object composed of a `validator` (a [`Validator`](https://layrjs.com/docs/v2/reference/validator) instance) and a `path` (a string representing the path of the attribute containing the validator that has failed).\n\n**Example:**\n\n```\n// See the `title` definition in the `validate()` example\n\ntitle.getValue(); // => 'Inception'\ntitle.runValidators(); // => []\ntitle.setValue('');\ntitle.runValidators(); // => [{validator: ..., path: ''}]\n```\n\n#### Observability\n\nSee the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) class.\n\n#### Utilities\n\n##### `isAttributeClass(value)` <badge type=\"tertiary-outline\">function</badge> {#is-attribute-class-function}\n\nReturns whether the specified value is an `Attribute` class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isAttributeInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-attribute-instance-function}\n\nReturns whether the specified value is an `Attribute` instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/attribute-selector-76ksImknctJspHLg12sRpR.immutable.md",
    "content": "### AttributeSelector <badge type=\"primary-outline\">type</badge> {#attribute-selector-type}\n\nAn `AttributeSelector` allows you to select some attributes of a component.\n\nThe simplest `AttributeSelector` is `true`, which means that all the attributes are selected.\n\nAnother possible `AttributeSelector` is `false`, which means that no attributes are selected.\n\nTo select some specific attributes, you can use a plain object where:\n\n* The keys are the name of the attributes you want to select.\n* The values are a boolean or a nested object to select some attributes of a nested component.\n\n**Examples:**\n\n```\n// Selects all the attributes\ntrue\n\n// Excludes all the attributes\nfalse\n\n// Selects `title`\n{title: true}\n\n// Selects also `title` (`summary` is not selected)\n{title: true, summary: false}\n\n// Selects `title` and `summary`\n{title: true, summary: true}\n\n// Selects `title`, `movieDetails.duration`, and `movieDetails.aspectRatio`\n{\n  title: true,\n  movieDetails: {\n    duration: true,\n    aspectRatio: true\n  }\n}\n```\n\n#### Functions\n\n##### `createAttributeSelectorFromNames(names)` <badge type=\"tertiary-outline\">function</badge> {#create-attribute-selector-from-names-function}\n\nCreates an `AttributeSelector` from the specified names.\n\n**Parameters:**\n\n* `names`: An array of strings.\n\n**Returns:**\n\nAn `AttributeSelector`.\n\n**Example:**\n\n```\ncreateAttributeSelectorFromNames(['title', 'summary']);\n// => {title: true, summary: true}\n```\n\n##### `createAttributeSelectorFromAttributes(attributes)` <badge type=\"tertiary-outline\">function</badge> {#create-attribute-selector-from-attributes-function}\n\nCreates an `AttributeSelector` from an attribute iterator.\n\n**Parameters:**\n\n* `attributes`: An [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) iterator.\n\n**Returns:**\n\nAn `AttributeSelector`.\n\n**Example:**\n\n```\ncreateAttributeSelectorFromAttributes(Movie.prototype.getAttributes());\n// => {title: true, summary: true, movieDetails: true}\n```\n\n##### `getFromAttributeSelector(attributeSelector, name)` <badge type=\"tertiary-outline\">function</badge> {#get-from-attribute-selector-function}\n\nGets an entry of an `AttributeSelector`.\n\n**Parameters:**\n\n* `attributeSelector`: An `AttributeSelector`.\n* `name`: The name of the entry to get.\n\n**Returns:**\n\nAn `AttributeSelector`.\n\n**Example:**\n\n```\ngetFromAttributeSelector(true, 'title');\n// => true\n\ngetFromAttributeSelector(false, 'title');\n// => false\n\ngetFromAttributeSelector({title: true}, 'title');\n// => true\n\ngetFromAttributeSelector({title: true}, 'summary');\n// => false\n\ngetFromAttributeSelector({movieDetails: {duration: true}}, 'movieDetails');\n// => {duration: true}\n```\n\n##### `setWithinAttributeSelector(attributeSelector, name, subattributeSelector)` <badge type=\"tertiary-outline\">function</badge> {#set-within-attribute-selector-function}\n\nReturns an `AttributeSelector` where an entry of the specified `AttributeSelector` is set with another `AttributeSelector`.\n\n**Parameters:**\n\n* `attributeSelector`: An `AttributeSelector`.\n* `name`: The name of the entry to set.\n* `subattributeSelector`: Another `AttributeSelector`.\n\n**Returns:**\n\nA new `AttributeSelector`.\n\n**Example:**\n\n```\nsetWithinAttributeSelector({title: true}, 'summary', true);\n// => {title: true, summary: true}\n\nsetWithinAttributeSelector({title: true}, 'summary', false);\n// => {title: true}\n\nsetWithinAttributeSelector({title: true, summary: true}, 'summary', false);\n// => {title: true}\n\nsetWithinAttributeSelector({title: true}, 'movieDetails', {duration: true});\n// => {title: true, movieDetails: {duration: true}}\n```\n\n##### `cloneAttributeSelector(attributeSelector)` <badge type=\"tertiary-outline\">function</badge> {#clone-attribute-selector-function}\n\nClones an `AttributeSelector`.\n\n**Parameters:**\n\n* `attributeSelector`: An `AttributeSelector`.\n\n**Returns:**\n\nA new `AttributeSelector`.\n\n**Example:**\n\n```\ncloneAttributeSelector(true);\n// => true\n\ncloneAttributeSelector(false);\n// => false\n\ncloneAttributeSelector({title: true, movieDetails: {duration: true});\n// => {title: true, movieDetails: {duration: true}\n```\n\n##### `attributeSelectorsAreEqual(attributeSelector, otherAttributeSelector)` <badge type=\"tertiary-outline\">function</badge> {#attribute-selectors-are-equal-function}\n\nReturns whether an `AttributeSelector` is equal to another `AttributeSelector`.\n\n**Parameters:**\n\n* `attributeSelector`: An `AttributeSelector`.\n* `otherAttributeSelector`: Another `AttributeSelector`.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nattributeSelectorsAreEqual({title: true}, {title: true});\n// => true\n\nattributeSelectorsAreEqual({title: true, summary: false}, {title: true});\n// => true\n\nattributeSelectorsAreEqual({title: true}, {summary: true});\n// => false\n```\n\n##### `attributeSelectorIncludes(attributeSelector, otherAttributeSelector)` <badge type=\"tertiary-outline\">function</badge> {#attribute-selector-includes-function}\n\nReturns whether an `AttributeSelector` includes another `AttributeSelector`.\n\n**Parameters:**\n\n* `attributeSelector`: An `AttributeSelector`.\n* `otherAttributeSelector`: Another `AttributeSelector`.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nattributeSelectorIncludes({title: true}, {title: true});\n// => true\n\nattributeSelectorIncludes({title: true, summary: true}, {title: true});\n// => true\n\nattributeSelectorIncludes({title: true}, {summary: true});\n// => false\n```\n\n##### `mergeAttributeSelectors(attributeSelector, otherAttributeSelector)` <badge type=\"tertiary-outline\">function</badge> {#merge-attribute-selectors-function}\n\nReturns an `AttributeSelector` which is the result of merging an `AttributeSelector` with another `AttributeSelector`.\n\n**Parameters:**\n\n* `attributeSelector`: An `AttributeSelector`.\n* `otherAttributeSelector`: Another `AttributeSelector`.\n\n**Returns:**\n\nA new `AttributeSelector`.\n\n**Example:**\n\n```\nmergeAttributeSelectors({title: true}, {title: true});\n// => {title: true}\n\nmergeAttributeSelectors({title: true}, {summary: true});\n// => {title: true, summary: true}\n\nmergeAttributeSelectors({title: true, summary: true}, {summary: false});\n// => {title: true}\n```\n\n##### `intersectAttributeSelectors(attributeSelector, otherAttributeSelector)` <badge type=\"tertiary-outline\">function</badge> {#intersect-attribute-selectors-function}\n\nReturns an `AttributeSelector` which is the result of the intersection of an `AttributeSelector` with another `AttributeSelector`.\n\n**Parameters:**\n\n* `attributeSelector`: An `AttributeSelector`.\n* `otherAttributeSelector`: Another `AttributeSelector`.\n\n**Returns:**\n\nA new `AttributeSelector`.\n\n**Example:**\n\n```\nintersectAttributeSelectors({title: true, summary: true}, {title: true});\n// => {title: true}\n\nintersectAttributeSelectors({title: true}, {summary: true});\n// => {}\n```\n\n##### `removeFromAttributeSelector(attributeSelector, otherAttributeSelector)` <badge type=\"tertiary-outline\">function</badge> {#remove-from-attribute-selector-function}\n\nReturns an `AttributeSelector` which is the result of removing an `AttributeSelector` from another `AttributeSelector`.\n\n**Parameters:**\n\n* `attributeSelector`: An `AttributeSelector`.\n* `otherAttributeSelector`: Another `AttributeSelector`.\n\n**Returns:**\n\nA new `AttributeSelector`.\n\n**Example:**\n\n```\nremoveFromAttributeSelector({title: true, summary: true}, {summary: true});\n// => {title: true}\n\nremoveFromAttributeSelector({title: true}, {title: true});\n// => {}\n```\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/aws-integration-3Rt2txpqioYQsmijgSqqwF.immutable.md",
    "content": "### aws-integration <badge type=\"primary\">module</badge> {#aws-integration-module}\n\nAllows you to host a [component server](https://layrjs.com/docs/v2/reference/component-server) in [AWS Lambda](https://aws.amazon.com/lambda/).\n\n#### Functions\n\n##### `createAWSLambdaHandler(componentServer)` <badge type=\"tertiary-outline\">function</badge> {#create-aws-lambda-handler-function}\n\nCreates an [AWS Lambda function handler](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html) for the specified [ComponentServer](https://layrjs.com/docs/v2/reference/component-server) instance.\n\nAlternatively, you can pass a [`Component`](https://layrjs.com/docs/v2/reference/component) class or a function (which can be asynchronous) returning a [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server) instance or a [`Component`](https://layrjs.com/docs/v2/reference/component) class. When a [`Component`](https://layrjs.com/docs/v2/reference/component) class is passed (or returned from a passed function), a [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server) instance is automatically created from the [`Component`](https://layrjs.com/docs/v2/reference/component) class.\n\nThe created handler can be hosted in [AWS Lambda](https://aws.amazon.com/lambda/) and consumed by [AWS API Gateway](https://aws.amazon.com/api-gateway/) through an [HTTP API](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api.html).\n\n**Parameters:**\n\n* `componentServer`: A [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server) instance, a [`Component`](https://layrjs.com/docs/v2/reference/component), or a function (which can be asynchronous) returning a [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server) instance or a [`Component`](https://layrjs.com/docs/v2/reference/component).\n\n**Returns:**\n\nAn [AWS Lambda function handler](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html).\n\n**Example:**\n\n```\nimport {Component, attribute, expose} from '@layr/component';\nimport {ComponentServer} from '@layr/component-server';\nimport {createAWSLambdaHandler} from '@layr/aws-integration';\n\nclass Movie extends Component {\n  @expose({get: true, set: true}) @attribute('string') title = '';\n}\n\nconst server = new ComponentServer(Movie);\n\nconst handler = createAWSLambdaHandler(server);\n\nexport {handler};\n```\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/browser-navigator-eYIxYKNFdRYiKTxIWAjqd.immutable.md",
    "content": "### BrowserNavigator <badge type=\"primary\">class</badge> {#browser-navigator-class}\n\n*Inherits from [`Navigator`](https://layrjs.com/docs/v2/reference/navigator).*\n\nA [`Navigator`](https://layrjs.com/docs/v2/reference/navigator) relying on the browser's [History API](https://developer.mozilla.org/en-US/docs/Web/API/History) to determine the current [route](https://layrjs.com/docs/v2/reference/route).\n\n#### Usage\n\nIf you are using [React](https://reactjs.org/), the easiest way to set up a `BrowserNavigator` in your app is to use the [`BrowserNavigatorView`](https://layrjs.com/docs/v2/reference/react-integration#browser-navigator-view-react-component) React component that is provided by the `@layr/react-integration` package.\n\nSee an example of use in the [`BrowserNavigatorView`](https://layrjs.com/docs/v2/reference/react-integration#browser-navigator-view-react-component) React component.\n\n#### Creation\n\n##### `new BrowserNavigator()` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates a [`BrowserNavigator`](https://layrjs.com/docs/v2/reference/browser-navigator).\n\n**Returns:**\n\nThe [`BrowserNavigator`](https://layrjs.com/docs/v2/reference/browser-navigator) instance that was created.\n\n#### Component Registration\n\nSee the methods that are inherited from the [`Navigator`](https://layrjs.com/docs/v2/reference/navigator#component-registration) class.\n\n#### Routes\n\nSee the methods that are inherited from the [`Navigator`](https://layrjs.com/docs/v2/reference/navigator#routes) class.\n\n#### Current Location\n\nSee the methods that are inherited from the [`Navigator`](https://layrjs.com/docs/v2/reference/navigator#current-location) class.\n\n#### Navigation\n\nSee the methods that are inherited from the [`Navigator`](https://layrjs.com/docs/v2/reference/navigator#navigation) class.\n\n#### Link Rendering\n\n##### `Link(props)` <badge type=\"secondary-outline\">instance method</badge> {#link-instance-method}\n\nRenders a link that is managed by the navigator.\n\nThis method is only available when you create your navigator by using the [`useBrowserNavigator()`](https://layrjs.com/docs/v2/reference/react-integration#use-browser-navigator-react-hook) React hook.\n\nNote that instead of using this method, you can use the handy `Link()` shortcut function that you get when you define a route with the [`@route()`](https://layrjs.com/docs/v2/reference/routable#route-decorator) decorator.\n\n**Parameters:**\n\n* `props`:\n  * `to`: A string representing the target URL of the link.\n  * `className`: A [`className`](https://reactjs.org/docs/dom-elements.html#classname) attribute to apply to the rendered link.\n  * `activeClassName`: An additional [`className`](https://reactjs.org/docs/dom-elements.html#classname) attribute to apply to the rendered link when the URL of the current navigator's route is the same as the target URL of the link.\n  * `style`: A [`style`](https://reactjs.org/docs/dom-elements.html#style) attribute to apply to the rendered link.\n  * `activeStyle`: An additional [`style`](https://reactjs.org/docs/dom-elements.html#style) attribute to apply to the rendered link when the URL of the current navigator's route is the same as the target URL of the link.\n\n**Returns:**\n\nAn `<a>` React element.\n\n**Example:**\n\n```\nclass Application extends Routable(Component) {\n   @route('/') @view static Home() {\n     const navigator = this.getNavigator();\n     return <navigator.Link to=\"/about-us\">About Us</navigator.Link>;\n\n     // Same as above, but in a more idiomatic way:\n     return <this.AboutUs.Link>About Us</this.AboutUs.Link>;\n   }\n\n   @route('/about-us') @view static AboutUs() {\n     return <div>Here is everything about us.<div>;\n   }\n}\n```\n\n#### Observability\n\nSee the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) class.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/component-1zBrZVglO2cjDP9aO87pOR.immutable.md",
    "content": "### Component <badge type=\"primary\">class</badge> {#component-class}\n\n*Inherits from [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class).*\n\nA component is an elementary building block allowing you to define your data models and implement the business logic of your app. Typically, an app is composed of several components that are connected to each other by using the [`@provide()`](https://layrjs.com/docs/v2/reference/component#provide-decorator) and [`@consume()`](https://layrjs.com/docs/v2/reference/component#consume-decorator) decorators.\n\n#### Usage\n\nJust extend the `Component` class to define a component with some attributes and methods that are specific to your app.\n\nFor example, a `Movie` component with a `title` attribute and a `play()` method could be defined as follows:\n\n```\n// JS\n\nimport {Component} from '@layr/component';\n\nclass Movie extends Component {\n  @attribute('string') title;\n\n  @method() play() {\n    console.log(`Playing '${this.title}...'`);\n  }\n}\n```\n\n```\n// TS\n\nimport {Component} from '@layr/component';\n\nclass Movie extends Component {\n  @attribute('string') title!: string;\n\n  @method() play() {\n    console.log(`Playing '${this.title}...'`);\n  }\n}\n```\n\nThe [`@attribute()`](https://layrjs.com/docs/v2/reference/component#attribute-decorator) and [`@method()`](https://layrjs.com/docs/v2/reference/component#method-decorator) decorators allows you to get the full power of Layr, such as attribute validation or remote method invocation.\n\n> Note that you don't need to use the [`@method()`](https://layrjs.com/docs/v2/reference/component#method-decorator) decorator for all your methods. Typically, you use this decorator only for some backend methods that you want to expose to the frontend.\n\nOnce you have defined a component, you can use it as any JavaScript/TypeScript class:\n\n```\nconst movie = new Movie({title: 'Inception'});\n\nmovie.play(); // => 'Playing Inception...'\n```\n\n#### Embedded Components\n\nUse the [`EmbeddedComponent`](https://layrjs.com/docs/v2/reference/embedded-component) class to embed a component into another component. An embedded component is strongly attached to the parent component that owns it, and it cannot \"live\" by itself like a regular component.\n\nHere are some characteristics of an embedded component:\n\n- An embedded component has one parent only, and therefore cannot be embedded in more than one component.\n- When the parent of an embedded component is [validated](https://layrjs.com/docs/v2/reference/validator), the embedded component is validated as well.\n- When the parent of an embedded component is loaded, saved, or deleted (using a [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storage-operations) method), the embedded component is loaded, saved, or deleted as well.\n\nSee the [`EmbeddedComponent`](https://layrjs.com/docs/v2/reference/embedded-component) class for an example of use.\n\n#### Referenced Components\n\nAny non-embedded component can be referenced by another component. Contrary to an embedded component, a referenced component is an independent entity that can \"live\" by itself. So a referenced component behaves like a regular JavaScript object that can be referenced by another object.\n\nHere are some characteristics of a referenced component:\n\n- A referenced component can be referenced by any number of components.\n- When a component holding a reference to another component is [validated](https://layrjs.com/docs/v2/reference/validator), the referenced component is considered as an independent entity, and is therefore not validated automatically.\n- When a component holding a reference to another component is loaded, saved, or deleted (using a [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storage-operations) method), the referenced component may optionally be loaded in the same operation, but it has to be saved or deleted independently.\n\nFor example, let's say we have a `Director` component defined as follows:\n\n```\n// JS\n\nclass Director extends Component {\n  @attribute('string') fullName;\n}\n```\n\n```\n// TS\n\nclass Director extends Component {\n  @attribute('string') fullName!: string;\n}\n```\n\nNext, we can add an attribute to the `Movie` component to store a reference to a `Director`:\n\n```\n// JS\n\nclass Movie extends Component {\n  @provide() static Director = Director;\n\n  // ...\n\n  @attribute('Director') director;\n}\n```\n\n```\n// TS\n\nclass Movie extends Component {\n  @provide() static Director = Director;\n\n  // ...\n\n  @attribute('Director') director!: Director;\n}\n```\n\n> Note that to be able to specify the `'Director'` type for the `director` attribute, you first have to make the `Movie` component aware of the `Director` component by using the [`@provide()`](https://layrjs.com/docs/v2/reference/component#provide-decorator) decorator.\n\n Then, to create a `Movie` with a `Director`, we can do something like the following:\n\n```\nconst movie = new Movie({\n  title: 'Inception',\n  director: new Director({fullName: 'Christopher Nolan'})\n});\n\nmovie.title; // => 'Inception'\nmovie.director.fullName; // => 'Christopher Nolan'\n```\n\n#### Creation\n\n##### `new Component([object])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates an instance of a component class.\n\n**Parameters:**\n\n* `object`: An optional object specifying the value of the component attributes.\n\n**Returns:**\n\nThe component instance that was created.\n\n**Example:**\n\n```\n// JS\n\nimport {Component, attribute} from '@layr/component';\n\nclass Movie extends Component {\n  @attribute('string') title;\n}\n\nconst movie = new Movie({title: 'Inception'});\n\nmovie.title // => 'Inception'\n```\n```\n// TS\n\nimport {Component, attribute} from '@layr/component';\n\nclass Movie extends Component {\n  @attribute('string') title!: string;\n}\n\nconst movie = new Movie({title: 'Inception'});\n\nmovie.title // => 'Inception'\n```\n\n#### Initialization\n\n##### `initialize()` <badge type=\"secondary\">class method</badge> <badge type=\"outline\">possibly async</badge> {#initialize-class-method}\n\nInvoke this method to call any `initializer()` static method defined in your component classes.\n\nIf the current class has an `initializer()` static method, it is invoked, and if the current class has some other components as dependencies, the `initializer()` method is also invoked for those components.\n\nNote that your `initializer()` methods can be asynchronous, and therefore you should call the `initialize()` method with `await`.\n\nTypically, you will call the `initialize()` method on the root component of your frontend service when your app starts. Backend services are usually managed by a [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server), which automatically invokes the `initialize()` method on the root component.\n\nNote that if you use [Boostr](https://boostr.dev) to manage your frontend service, you should not call the `initialize()` method manually.\n\n#### Naming\n\n##### `getComponentName()` <badge type=\"secondary\">class method</badge> {#get-component-name-class-method}\n\nReturns the name of the component, which is usually the name of the corresponding class.\n\n**Returns:**\n\nA string.\n\n**Example:**\n\n```\nMovie.getComponentName(); // => 'Movie'\n```\n\n##### `setComponentName(name)` <badge type=\"secondary\">class method</badge> {#set-component-name-class-method}\n\nSets the name of the component. As the name of a component is usually inferred from the name of its class, this method should rarely be used.\n\n**Parameters:**\n\n* `name`: The name you wish for the component.\n\n**Example:**\n\n```\nMovie.getComponentName(); // => 'Movie'\nMovie.setComponentName('Film');\nMovie.getComponentName(); // => 'Film'\n```\n\n##### `getComponentPath()` <badge type=\"secondary\">class method</badge> {#get-component-path-class-method}\n\nReturns the path of the component starting from its root component.\n\nFor example, if an `Application` component provides a `Movie` component, this method will return `'Application.Movie'` when called on the `Movie` component.\n\n**Returns:**\n\nA string.\n\n**Example:**\n\n```\nclass Movie extends Component {}\n\nMovie.getComponentPath(); // => 'Movie'\n\nclass Application extends Component {\n  @provide() static Movie = Movie;\n}\n\nMovie.getComponentPath(); // => 'Application.Movie'\n```\n\n#### Typing\n\n##### `getComponentType()` <badge type=\"secondary\">class method</badge> {#get-component-type-class-method}\n\nReturns the type of the component class. A component class type is composed of the component class name prefixed with the string `'typeof '`.\n\nFor example, with a component class named `'Movie'`, this method will return `'typeof Movie'`.\n\n**Returns:**\n\nA string.\n\n**Example:**\n\n```\nMovie.getComponentType(); // => 'typeof Movie'\n```\n\n##### `getComponentType()` <badge type=\"secondary-outline\">instance method</badge> {#get-component-type-instance-method}\n\nReturns the type of the component instance. A component instance type is equivalent to the component class name.\n\nFor example, with a component class named `'Movie'`, this method will return `'Movie'` when called on a `Movie` instance.\n\n**Returns:**\n\nA string.\n\n**Example:**\n\n```\nMovie.prototype.getComponentType(); // => 'Movie'\n\nconst movie = new Movie();\nmovie.getComponentType(); // => 'Movie'\n```\n\n#### isNew Mark\n\n##### `getIsNewMark()` <badge type=\"secondary-outline\">instance method</badge> {#get-is-new-mark-instance-method}\n\nReturns whether the component instance is marked as new or not.\n\n**Alias:**\n\n`isNew()`\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nlet movie = new Movie();\nmovie.getIsNewMark(); // => true\n```\n\n##### `setIsNewMark(isNew)` <badge type=\"secondary-outline\">instance method</badge> {#set-is-new-mark-instance-method}\n\nSets whether the component instance is marked as new or not.\n\n**Parameters:**\n\n* `isNew`: A boolean specifying if the component instance should be marked as new or not.\n\n**Example:**\n\n```\nconst movie = new Movie();\nmovie.getIsNewMark(); // => true\nmovie.setIsNewMark(false);\nmovie.getIsNewMark(); // => false\n```\n\n##### `isNew()` <badge type=\"secondary-outline\">instance method</badge> {#is-new-instance-method}\n\nReturns whether the component instance is marked as new or not.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nlet movie = new Movie();\nmovie.isNew(); // => true\n```\n\n##### `markAsNew()` <badge type=\"secondary-outline\">instance method</badge> {#mark-as-new-instance-method}\n\nMarks the component instance as new.\n\nThis method is a shortcut for `setIsNewMark(true)`.\n\n##### `markAsNotNew()` <badge type=\"secondary-outline\">instance method</badge> {#mark-as-not-new-instance-method}\n\nMarks the component instance as not new.\n\nThis method is a shortcut for `setIsNewMark(false)`.\n\n#### Observability\n\nSee the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) class.\n\n#### Embeddability\n\n##### `isEmbedded()` <badge type=\"secondary\">class method</badge> {#is-embedded-class-method}\n\nReturns whether the component is an [`EmbeddedComponent`](https://layrjs.com/docs/v2/reference/embedded-component).\n\n**Returns:**\n\nA boolean.\n\n#### Properties\n\n##### `getProperty(name)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#get-property-dual-method}\n\nGets a property of the component.\n\n**Parameters:**\n\n* `name`: The name of the property to get.\n\n**Returns:**\n\nAn instance of a [`Property`](https://layrjs.com/docs/v2/reference/property) (or a subclass of [`Property`](https://layrjs.com/docs/v2/reference/property) such as [`Attribute`](https://layrjs.com/docs/v2/reference/attribute), [`Method`](https://layrjs.com/docs/v2/reference/method), etc.).\n\n**Example:**\n\n```\nmovie.getProperty('title'); // => 'title' attribute property\nmovie.getProperty('play'); // => 'play()' method property\n```\n\n##### `hasProperty(name)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#has-property-dual-method}\n\nReturns whether the component has the specified property.\n\n**Parameters:**\n\n* `name`: The name of the property to check.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nmovie.hasProperty('title'); // => true\nmovie.hasProperty('play'); // => true\nmovie.hasProperty('name'); // => false\n```\n\n##### `setProperty(name, PropertyClass, [propertyOptions])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#set-property-dual-method}\n\nDefines a property in the component. Typically, instead of using this method, you would rather use a decorator such as [`@attribute()`](https://layrjs.com/docs/v2/reference/component#attribute-decorator) or [`@method()`](https://layrjs.com/docs/v2/reference/component#method-decorator).\n\n**Parameters:**\n\n* `name`: The name of the property to define.\n* `PropertyClass`: The class of the property (e.g., [`Attribute`](https://layrjs.com/docs/v2/reference/attribute), [`Method`](https://layrjs.com/docs/v2/reference/method)) to use.\n* `propertyOptions`: The options to create the `PropertyClass`.\n\n**Returns:**\n\nThe property that was created.\n\n**Example:**\n\n```\nMovie.prototype.setProperty('title', Attribute, {valueType: 'string'});\n```\n\n##### `deleteProperty(name)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#delete-property-dual-method}\n\nRemoves a property from the component. If the specified property doesn't exist, nothing happens.\n\n**Parameters:**\n\n* `name`: The name of the property to remove.\n\n**Returns:**\n\nA boolean.\n\n##### `getProperties([options])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#get-properties-dual-method}\n\nReturns an iterator providing the properties of the component.\n\n**Parameters:**\n\n* `options`:\n  * `filter`: A function used to filter the properties to be returned. The function is invoked for each property with a [`Property`](https://layrjs.com/docs/v2/reference/property) instance as first argument.\n  * `attributesOnly`: A boolean specifying whether only attribute properties should be returned (default: `false`).\n  * `setAttributesOnly`: A boolean specifying whether only set attributes should be returned (default: `false`).\n  * `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be returned (default: `true`, which means that all the attributes should be returned).\n  * `methodsOnly`: A boolean specifying whether only method properties should be returned (default: `false`).\n\n**Returns:**\n\nA [`Property`](https://layrjs.com/docs/v2/reference/property) instance iterator.\n\n**Example:**\n\n```\nfor (const property of movie.getProperties()) {\n  console.log(property.getName());\n}\n\n// Should output:\n// title\n// play\n```\n\n##### `getPropertyNames()` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#get-property-names-dual-method}\n\nReturns the name of all the properties of the component.\n\n**Returns:**\n\nAn array of the property names.\n\n**Example:**\n\n```\nmovie.getPropertyNames(); // => ['title', 'play']\n```\n\n#### Attribute Properties\n\n##### `getAttribute(name)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#get-attribute-dual-method}\n\nGets an attribute of the component.\n\n**Parameters:**\n\n* `name`: The name of the attribute to get.\n\n**Returns:**\n\nAn instance of [`Attribute`](https://layrjs.com/docs/v2/reference/attribute).\n\n**Example:**\n\n```\nmovie.getAttribute('title'); // => 'title' attribute property\nmovie.getAttribute('play'); // => Error ('play' is a method)\n```\n\n##### `hasAttribute(name)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#has-attribute-dual-method}\n\nReturns whether the component has the specified attribute.\n\n**Parameters:**\n\n* `name`: The name of the attribute to check.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nmovie.hasAttribute('title'); // => true\nmovie.hasAttribute('name'); // => false\nmovie.hasAttribute('play'); // => Error ('play' is a method)\n```\n\n##### `setAttribute(name, [attributeOptions])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#set-attribute-dual-method}\n\nDefines an attribute in the component. Typically, instead of using this method, you would rather use the [`@attribute()`](https://layrjs.com/docs/v2/reference/component#attribute-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the attribute to define.\n* `attributeOptions`: The options to create the [`Attribute`](https://layrjs.com/docs/v2/reference/attribute#constructor).\n\n**Returns:**\n\nThe [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) that was created.\n\n**Example:**\n\n```\nMovie.prototype.setAttribute('title', {valueType: 'string'});\n```\n\n##### `getAttributes([options])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#get-attributes-dual-method}\n\nReturns an iterator providing the attributes of the component.\n\n**Parameters:**\n\n* `options`:\n  * `filter`: A function used to filter the attributes to be returned. The function is invoked for each attribute with an [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) instance as first argument.\n  * `setAttributesOnly`: A boolean specifying whether only set attributes should be returned (default: `false`).\n  * `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be returned (default: `true`, which means that all the attributes should be returned).\n\n**Returns:**\n\nAn [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) instance iterator.\n\n**Example:**\n\n```\nfor (const attr of movie.getAttributes()) {\n  console.log(attr.getName());\n}\n\n// Should output:\n// title\n```\n\n##### `getIdentifierAttribute(name)` <badge type=\"secondary-outline\">instance method</badge> {#get-identifier-attribute-instance-method}\n\nGets an identifier attribute of the component.\n\n**Parameters:**\n\n* `name`: The name of the identifier attribute to get.\n\n**Returns:**\n\nAn instance of [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute) or [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute).\n\n**Example:**\n\n```\nmovie.getIdentifierAttribute('id'); // => 'id' primary identifier attribute\nmovie.getIdentifierAttribute('slug'); // => 'slug' secondary identifier attribute\n```\n\n##### `hasIdentifierAttribute(name)` <badge type=\"secondary-outline\">instance method</badge> {#has-identifier-attribute-instance-method}\n\nReturns whether the component has the specified identifier attribute.\n\n**Parameters:**\n\n* `name`: The name of the identifier attribute to check.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nmovie.hasIdentifierAttribute('id'); // => true\nmovie.hasIdentifierAttribute('slug'); // => true\nmovie.hasIdentifierAttribute('name'); // => false (the property 'name' doesn't exist)\nmovie.hasIdentifierAttribute('title'); // => Error ('title' is not an identifier attribute)\n```\n\n##### `getPrimaryIdentifierAttribute()` <badge type=\"secondary-outline\">instance method</badge> {#get-primary-identifier-attribute-instance-method}\n\nGets the primary identifier attribute of the component.\n\n**Returns:**\n\nAn instance of [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute).\n\n**Example:**\n\n```\nmovie.getPrimaryIdentifierAttribute(); // => 'id' primary identifier attribute\n```\n\n##### `hasPrimaryIdentifierAttribute()` <badge type=\"secondary-outline\">instance method</badge> {#has-primary-identifier-attribute-instance-method}\n\nReturns whether the component as a primary identifier attribute.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nmovie.hasPrimaryIdentifierAttribute(); // => true\n```\n\n##### `setPrimaryIdentifierAttribute(name, [attributeOptions])` <badge type=\"secondary-outline\">instance method</badge> {#set-primary-identifier-attribute-instance-method}\n\nDefines the primary identifier attribute of the component. Typically, instead of using this method, you would rather use the [`@primaryIdentifier()`](https://layrjs.com/docs/v2/reference/component#primary-identifier-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the primary identifier attribute to define.\n* `attributeOptions`: The options to create the [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute).\n\n**Returns:**\n\nThe [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute) that was created.\n\n**Example:**\n\n```\nUser.prototype.setPrimaryIdentifierAttribute('id', {\n  valueType: 'number',\n  default() {\n    return Math.random();\n  }\n});\n```\n\n##### `getSecondaryIdentifierAttribute(name)` <badge type=\"secondary-outline\">instance method</badge> {#get-secondary-identifier-attribute-instance-method}\n\nGets a secondary identifier attribute of the component.\n\n**Parameters:**\n\n* `name`: The name of the secondary identifier attribute to get.\n\n**Returns:**\n\nA [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute) instance.\n\n**Example:**\n\n```\nmovie.getSecondaryIdentifierAttribute('slug'); // => 'slug' secondary identifier attribute\nmovie.getSecondaryIdentifierAttribute('id'); // => Error ('id' is not a secondary identifier attribute)\n```\n\n##### `hasSecondaryIdentifierAttribute(name)` <badge type=\"secondary-outline\">instance method</badge> {#has-secondary-identifier-attribute-instance-method}\n\nReturns whether the component has the specified secondary identifier attribute.\n\n**Parameters:**\n\n* `name`: The name of the secondary identifier attribute to check.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nmovie.hasSecondaryIdentifierAttribute('slug'); // => true\nmovie.hasSecondaryIdentifierAttribute('name'); // => false (the property 'name' doesn't exist)\nmovie.hasSecondaryIdentifierAttribute('id'); // => Error ('id' is not a secondary identifier attribute)\n```\n\n##### `setSecondaryIdentifierAttribute(name, [attributeOptions])` <badge type=\"secondary-outline\">instance method</badge> {#set-secondary-identifier-attribute-instance-method}\n\nDefines a secondary identifier attribute in the component. Typically, instead of using this method, you would rather use the [`@secondaryIdentifier()`](https://layrjs.com/docs/v2/reference/component#secondary-identifier-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the secondary identifier attribute to define.\n* `attributeOptions`: The options to create the [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute).\n\n**Returns:**\n\nThe [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute) that was created.\n\n**Example:**\n\n```\nUser.prototype.setSecondaryIdentifierAttribute('slug', {valueType: 'string'});\n```\n\n##### `getIdentifierAttributes([options])` <badge type=\"secondary-outline\">instance method</badge> {#get-identifier-attributes-instance-method}\n\nReturns an iterator providing the identifier attributes of the component.\n\n**Parameters:**\n\n* `options`:\n  * `filter`: A function used to filter the identifier attributes to be returned. The function is invoked for each identifier attribute with an `IdentifierAttribute` instance as first argument.\n  * `setAttributesOnly`: A boolean specifying whether only set identifier attributes should be returned (default: `false`).\n  * `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the identifier attributes to be returned (default: `true`, which means that all identifier attributes should be returned).\n\n**Returns:**\n\nAn iterator of [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute) or [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute).\n\n**Example:**\n\n```\nfor (const attr of movie.getIdentifierAttributes()) {\n  console.log(attr.getName());\n}\n\n// Should output:\n// id\n// slug\n```\n\n##### `getSecondaryIdentifierAttributes([options])` <badge type=\"secondary-outline\">instance method</badge> {#get-secondary-identifier-attributes-instance-method}\n\nReturns an iterator providing the secondary identifier attributes of the component.\n\n**Parameters:**\n\n* `options`:\n  * `filter`: A function used to filter the secondary identifier attributes to be returned. The function is invoked for each identifier attribute with a [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute) instance as first argument.\n  * `setAttributesOnly`: A boolean specifying whether only set secondary identifier attributes should be returned (default: `false`).\n  * `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the secondary identifier attributes to be returned (default: `true`, which means that all secondary identifier attributes should be returned).\n\n**Returns:**\n\nA [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute) instance iterator.\n\n**Example:**\n\n```\nfor (const attr of movie.getSecondaryIdentifierAttributes()) {\n  console.log(attr.getName());\n}\n\n// Should output:\n// slug\n```\n\n##### `getIdentifiers()` <badge type=\"secondary-outline\">instance method</badge> {#get-identifiers-instance-method}\n\nReturns an object composed of all the identifiers that are set in the component. The shape of the returned object is `{[identifierName]: identifierValue}`. Throws an error if the component doesn't have any set identifiers.\n\n**Returns:**\n\nAn object.\n\n**Example:**\n\n```\nmovie.getIdentifiers(); // => {id: 'abc123', slug: 'inception'}\n```\n\n##### `hasIdentifiers()` <badge type=\"secondary-outline\">instance method</badge> {#has-identifiers-instance-method}\n\nReturns whether the component has an identifier that is set or not.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nmovie.hasIdentifiers(); // => true\n```\n\n##### `generateId()` <badge type=\"secondary\">class method</badge> {#generate-id-class-method}\n\nGenerates a unique identifier using the [cuid](https://github.com/ericelliott/cuid) library.\n\n**Returns:**\n\nThe generated identifier.\n\n**Example:**\n\n```\nMovie.generateId(); // => 'ck41vli1z00013h5xx1esffyn'\n```\n\n#### Identifier Descriptor\n\n##### `getIdentifierDescriptor()` <badge type=\"secondary-outline\">instance method</badge> {#get-identifier-descriptor-instance-method}\n\nReturns the `IdentifierDescriptor` of the component.\n\nAn `IdentifierDescriptor` is a plain object composed of one pair of name/value corresponding to the name and value of the first identifier attribute encountered in a component. Usually it is the primary identifier, but if the primary identifier is not set, it can be a secondary identifier.\n\nIf there is no set identifier in the component, an error is thrown.\n\n**Returns:**\n\nAn object.\n\n**Example:**\n\n```\nmovie.getIdentifierDescriptor(); // => {id: 'abc123'}\n```\n\n##### `hasIdentifierDescriptor()` <badge type=\"secondary-outline\">instance method</badge> {#has-identifier-descriptor-instance-method}\n\nReturns whether the component can provide an `IdentifierDescriptor` (using the [`getIdentifierDescriptor()`](https://layrjs.com/docs/v2/reference/component#get-identifier-descriptor-instance-method) method) or not.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nmovie.hasIdentifierDescriptor(); // => true\n```\n\n#### Attribute Value Assignment\n\n##### `assign(object, [options])` <badge type=\"secondary\">class method</badge> {#assign-class-method}\n\nAssigns the specified attribute values to the current component class.\n\n**Parameters:**\n\n* `object`: An object specifying the attribute values to assign.\n* `options`:\n  * `source`: A string specifying the [source](https://layrjs.com/docs/v2/reference/attribute#value-source-type) of the attribute values (default: `'local'`).\n\n**Returns:**\n\nThe current component class.\n\n**Example:**\n\n```\nimport {Component, attribute} from '@layr/component';\n\nclass Application extends Component {\n  @attribute('string') static language = 'en';\n}\n\nApplication.language // => 'en'\n\nApplication.assign({language: 'fr'});\n\nApplication.language // => 'fr'\n```\n\n##### `assign(object, [options])` <badge type=\"secondary-outline\">instance method</badge> {#assign-instance-method}\n\nAssigns the specified attribute values to the current component instance.\n\n**Parameters:**\n\n* `object`: An object specifying the attribute values to assign.\n* `options`:\n  * `source`: A string specifying the [source](https://layrjs.com/docs/v2/reference/attribute#value-source-type) of the attribute values (default: `'local'`).\n\n**Returns:**\n\nThe current component instance.\n\n**Example:**\n\n```\n// JS\n\nimport {Component, attribute} from '@layr/component';\n\nclass Movie extends Component {\n  @attribute('string') title;\n  @attribute('number') rating;\n}\n\nconst movie = new Movie({title: 'Inception', rating: 8.7});\n\nmovie.title // => 'Inception'\nmovie.rating // => 8.7\n\nmovie.assign({rating: 8.8});\n\nmovie.title // => 'Inception'\nmovie.rating // => 8.8\n```\n```\n// TS\n\nimport {Component, attribute} from '@layr/component';\n\nclass Movie extends Component {\n  @attribute('string') title!: string;\n  @attribute('number') rating!: number;\n}\n\nconst movie = new Movie({title: 'Inception', rating: 8.7});\n\nmovie.title // => 'Inception'\nmovie.rating // => 8.7\n\nmovie.assign({rating: 8.8});\n\nmovie.rating // => 8.8\n```\n\n#### Identity Mapping\n\n##### `getIdentityMap()` <badge type=\"secondary\">class method</badge> {#get-identity-map-class-method}\n\nGets the [`IdentityMap`](https://layrjs.com/docs/v2/reference/identity-map) of the component.\n\n**Returns:**\n\nAn [`IdentityMap`](https://layrjs.com/docs/v2/reference/identity-map) instance.\n\n##### `attach()` <badge type=\"secondary\">class method</badge> {#attach-class-method}\n\nAttaches the component class to its [`IdentityMap`](https://layrjs.com/docs/v2/reference/identity-map). By default, all component classes are attached, so unless you have detached a component class earlier, you should not have to use this method.\n\n**Returns:**\n\nThe component class.\n\n##### `detach()` <badge type=\"secondary\">class method</badge> {#detach-class-method}\n\nDetaches the component class from its [`IdentityMap`](https://layrjs.com/docs/v2/reference/identity-map).\n\n**Returns:**\n\nThe component class.\n\n##### `isAttached()` <badge type=\"secondary\">class method</badge> {#is-attached-class-method}\n\nReturns whether the component class is attached to its [`IdentityMap`](https://layrjs.com/docs/v2/reference/identity-map).\n\n**Returns:**\n\nA boolean.\n\n##### `isDetached()` <badge type=\"secondary\">class method</badge> {#is-detached-class-method}\n\nReturns whether the component class is detached from its [`IdentityMap`](https://layrjs.com/docs/v2/reference/identity-map).\n\n**Returns:**\n\nA boolean.\n\n##### `attach()` <badge type=\"secondary-outline\">instance method</badge> {#attach-instance-method}\n\nAttaches the component instance to its [`IdentityMap`](https://layrjs.com/docs/v2/reference/identity-map). By default, all component instances are attached, so unless you have detached a component instance earlier, you should not have to use this method.\n\n**Returns:**\n\nThe component instance.\n\n##### `detach()` <badge type=\"secondary-outline\">instance method</badge> {#detach-instance-method}\n\nDetaches the component instance from its [`IdentityMap`](https://layrjs.com/docs/v2/reference/identity-map).\n\n**Returns:**\n\nThe component instance.\n\n##### `isAttached()` <badge type=\"secondary-outline\">instance method</badge> {#is-attached-instance-method}\n\nReturns whether the component instance is attached to its [`IdentityMap`](https://layrjs.com/docs/v2/reference/identity-map).\n\n**Returns:**\n\nA boolean.\n\n##### `isDetached()` <badge type=\"secondary-outline\">instance method</badge> {#is-detached-instance-method}\n\nReturns whether the component instance is detached from its [`IdentityMap`](https://layrjs.com/docs/v2/reference/identity-map).\n\n**Returns:**\n\nA boolean.\n\n#### Validation\n\n##### `validate([attributeSelector])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#validate-dual-method}\n\nValidates the attributes of the component. If an attribute doesn't pass the validation, an error is thrown. The error is a JavaScript `Error` instance with a `failedValidators` custom attribute which contains the result of the [`runValidators()`](https://layrjs.com/docs/v2/reference/component#run-validators-dual-method) method.\n\n**Parameters:**\n\n* `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be validated (default: `true`, which means that all the attributes will be validated).\n\n**Example:**\n\n```\n// JS\n\nimport {Component, attribute, validators} from '@layr/component';\n\nconst {notEmpty} = validators;\n\nclass Movie extends Component {\n  @attribute('string', {validators: [notEmpty()]}) title;\n}\n\nconst movie = new Movie({title: 'Inception'});\n\nmovie.title; // => 'Inception'\nmovie.validate(); // All good!\nmovie.title = '';\nmovie.validate(); // Error {failedValidators: [{validator: ..., path: 'title'}]}\n```\n```\n// TS\n\nimport {Component, attribute, validators} from '@layr/component';\n\nconst {notEmpty} = validators;\n\nclass Movie extends Component {\n  @attribute('string', {validators: [notEmpty()]}) title!: string;\n}\n\nconst movie = new Movie({title: 'Inception'});\n\nmovie.title; // => 'Inception'\nmovie.validate(); // All good!\nmovie.title = '';\nmovie.validate(); // Error {failedValidators: [{validator: ..., path: 'title'}]}\n```\n\n##### `isValid([attributeSelector])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#is-valid-dual-method}\n\nReturns whether the attributes of the component are valid.\n\n**Parameters:**\n\n* `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be checked (default: `true`, which means that all the attributes will be checked).\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\n// See the `movie` definition in the `validate()` example\n\nmovie.title; // => 'Inception'\nmovie.isValid(); // => true\nmovie.title = '';\nmovie.isValid(); // => false\n```\n\n##### `runValidators([attributeSelector])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#run-validators-dual-method}\n\nRuns the validators for all the set attributes of the component.\n\n**Parameters:**\n\n* `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be validated (default: `true`, which means that all the attributes will be validated).\n\n**Returns:**\n\nAn array containing the validators that have failed. Each item is a plain object composed of a `validator` (a [`Validator`](https://layrjs.com/docs/v2/reference/validator) instance) and a `path` (a string representing the path of the attribute containing the validator that has failed).\n\n**Example:**\n\n```\n// See the `movie` definition in the `validate()` example\n\nmovie.title; // => 'Inception'\nmovie.runValidators(); // => []\nmovie.title = '';\nmovie.runValidators(); // => [{validator: ..., path: 'title'}]\n```\n\n#### Method Properties\n\n##### `getMethod(name)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#get-method-dual-method}\n\nGets a method of the component.\n\n**Parameters:**\n\n* `name`: The name of the method to get.\n\n**Returns:**\n\nA [`Method`](https://layrjs.com/docs/v2/reference/method) instance.\n\n**Example:**\n\n```\nmovie.getMethod('play'); // => 'play' method property\nmovie.getMethod('title'); // => Error ('title' is an attribute property)\n```\n\n##### `hasMethod(name)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#has-method-dual-method}\n\nReturns whether the component has the specified method.\n\n**Parameters:**\n\n* `name`: The name of the method to check.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nmovie.hasMethod('play'); // => true\nmovie.hasMethod('destroy'); // => false\nmovie.hasMethod('title'); // => Error ('title' is an attribute property)\n```\n\n##### `setMethod(name, [methodOptions])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#set-method-dual-method}\n\nDefines a method in the component. Typically, instead of using this method, you would rather use the [`@method()`](https://layrjs.com/docs/v2/reference/component#method-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the method to define.\n* `methodOptions`: The options to create the [`Method`](https://layrjs.com/docs/v2/reference/method#constructor).\n\n**Returns:**\n\nThe [`Method`](https://layrjs.com/docs/v2/reference/method) that was created.\n\n**Example:**\n\n```\nMovie.prototype.setMethod('play');\n```\n\n##### `getMethods([options])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#get-methods-dual-method}\n\nReturns an iterator providing the methods of the component.\n\n**Parameters:**\n\n* `options`:\n  * `filter`: A function used to filter the methods to be returned. The function is invoked for each method with a [`Method`](https://layrjs.com/docs/v2/reference/method) instance as first argument.\n\n**Returns:**\n\nA [`Method`](https://layrjs.com/docs/v2/reference/method) instance iterator.\n\n**Example:**\n\n```\nfor (const meth of movie.getMethods()) {\n  console.log(meth.getName());\n}\n\n// Should output:\n// play\n```\n\n#### Dependency Management\n\n##### `getComponent(name)` <badge type=\"secondary\">class method</badge> {#get-component-class-method}\n\nGets a component class that is provided or consumed by the current component. An error is thrown if there is no component matching the specified name. If the specified name is the name of the current component, the latter is returned.\n\n**Parameters:**\n\n* `name`: The name of the component class to get.\n\n**Returns:**\n\nA component class.\n\n**Example:**\n\n```\nclass Application extends Component {\n  @provide() static Movie = Movie;\n}\n\nApplication.getComponent('Movie'); // => Movie\nApplication.getComponent('Application'); // => Application\n```\n\n##### `hasComponent(name)` <badge type=\"secondary\">class method</badge> {#has-component-class-method}\n\nReturns whether the current component provides or consumes another component.\n\n**Parameters:**\n\n* `name`: The name of the component class to check.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nclass Application extends Component {\n  @provide() static Movie = Movie;\n}\n\nApplication.hasComponent('Movie'); // => true\nApplication.hasComponent('Application'); // => true\nApplication.hasComponent('Film'); // => false\n```\n\n##### `getComponentOfType(type)` <badge type=\"secondary\">class method</badge> {#get-component-of-type-class-method}\n\nGets a component class or prototype of the specified type that is provided or consumed by the current component. An error is thrown if there is no component matching the specified type. If the specified type is the type of the current component, the latter is returned.\n\n**Parameters:**\n\n* `type`: The type of the component class or prototype to get.\n\n**Returns:**\n\nA component class or prototype.\n\n**Example:**\n\n```\nclass Application extends Component {\n  @provide() static Movie = Movie;\n}\n\nApplication.getComponentOfType('typeof Movie'); // => Movie\nApplication.getComponentOfType('Movie'); // => Movie.prototype\nApplication.getComponentOfType('typeof Application'); // => Application\nApplication.getComponentOfType('Application'); // => Application.prototype\n```\n\n##### `hasComponentOfType(type)` <badge type=\"secondary\">class method</badge> {#has-component-of-type-class-method}\n\nReturns whether the current component provides or consumes a component class or prototype matching the specified type.\n\n**Parameters:**\n\n* `type`: The type of the component class or prototype to check.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nclass Application extends Component {\n  @provide() static Movie = Movie;\n}\n\nApplication.hasComponentOfType('typeof Movie'); // => true\nApplication.hasComponentOfType('Movie'); // => true\nApplication.hasComponentOfType('typeof Application'); // => true\nApplication.hasComponentOfType('Application'); // => true\nApplication.hasComponentOfType('typeof Film'); // => false\nApplication.hasComponentOfType('Film'); // => false\n```\n\n##### `getProvidedComponent(name)` <badge type=\"secondary\">class method</badge> {#get-provided-component-class-method}\n\nGets a component that is provided by the current component. An error is thrown if there is no provided component with the specified name.\n\n**Parameters:**\n\n* `name`: The name of the provided component to get.\n\n**Returns:**\n\nA component class.\n\n**Example:**\n\n```\nclass Application extends Component {\n  @provide() static Movie = Movie;\n}\n\nApplication.getProvidedComponent('Movie'); // => Movie\n```\n\n##### `provideComponent(component)` <badge type=\"secondary\">class method</badge> {#provide-component-class-method}\n\nSpecifies that the current component is providing another component so it can be easily accessed from the current component or from any component that is \"consuming\" it using the [`consumeComponent()`](https://layrjs.com/docs/v2/reference/component#consume-component-class-method) method or the [`@consume()`](https://layrjs.com/docs/v2/reference/component#consume-decorator) decorator.\n\nThe provided component can later be accessed using a component accessor that was automatically set on the component provider.\n\nTypically, instead of using this method, you would rather use the [`@provide()`]((https://layrjs.com/docs/v2/reference/component#provide-decorator)) decorator.\n\n**Parameters:**\n\n* `component`: The component class to provide.\n\n**Example:**\n\n```\nclass Application extends Component {}\nclass Movie extends Component {}\nApplication.provideComponent(Movie);\n\nApplication.Movie; // => `Movie` class\n```\n\n##### `getProvidedComponents([options])` <badge type=\"secondary\">class method</badge> {#get-provided-components-class-method}\n\nReturns an iterator allowing to iterate over the components provided by the current component.\n\n**Parameters:**\n\n* `options`:\n  * `filter`: A function used to filter the provided components to be returned. The function is invoked for each provided component with the provided component as first argument.\n  * `deep`: A boolean specifying whether the method should get the provided components recursively (i.e., get the provided components of the provided components). Default: `false`.\n\n**Returns:**\n\nA component iterator.\n\n##### `getComponentProvider()` <badge type=\"secondary\">class method</badge> {#get-component-provider-class-method}\n\nReturns the provider of the component. If there is no component provider, returns the current component.\n\n**Returns:**\n\nA component provider.\n\n**Example:**\n\n```\nclass Application extends Component {}\nclass Movie extends Component {}\nApplication.provideComponent(Movie);\n\nMovie.getComponentProvider(); // => `Application` class\nApplication.getComponentProvider(); // => `Application` class\n```\n\n##### `getConsumedComponent(name)` <badge type=\"secondary\">class method</badge> {#get-consumed-component-class-method}\n\nGets a component that is consumed by the current component. An error is thrown if there is no consumed component with the specified name. Typically, instead of using this method, you would rather use the component accessor that has been automatically set for you.\n\n**Parameters:**\n\n* `name`: The name of the consumed component to get.\n\n**Returns:**\n\nA component class.\n\n**Example:**\n\n```\n// JS\n\nclass Movie extends Component {\n  @consume() static Actor;\n}\n\nclass Actor extends Component {}\n\nclass Application extends Component {\n  @provide() static Movie = Movie;\n  @provide() static Actor = Actor;\n}\n\nMovie.getConsumedComponent('Actor'); // => Actor\n\n// Typically, you would rather use the component accessor:\nMovie.Actor; // => Actor\n```\n```\n// TS\n\nclass Movie extends Component {\n  @consume() static Actor: typeof Actor;\n}\n\nclass Actor extends Component {}\n\nclass Application extends Component {\n  @provide() static Movie = Movie;\n  @provide() static Actor = Actor;\n}\n\nMovie.getConsumedComponent('Actor'); // => Actor\n\n// Typically, you would rather use the component accessor:\nMovie.Actor; // => Actor\n```\n\n##### `consumeComponent(name)` <badge type=\"secondary\">class method</badge> {#consume-component-class-method}\n\nSpecifies that the current component is consuming another component so it can be easily accessed using a component accessor.\n\nTypically, instead of using this method, you would rather use the [`@consume()`]((https://layrjs.com/docs/v2/reference/component#consume-decorator)) decorator.\n\n**Parameters:**\n\n* `name`: The name of the component to consume.\n\n**Example:**\n\n```\nclass Application extends Component {}\nclass Movie extends Component {}\nApplication.provideComponent(Movie);\nMovie.consumeComponent('Application');\n\nMovie.Application; // => `Application` class\n```\n\n##### `getConsumedComponents([options])` <badge type=\"secondary\">class method</badge> {#get-consumed-components-class-method}\n\nReturns an iterator allowing to iterate over the components consumed by the current component.\n\n**Parameters:**\n\n* `options`:\n  * `filter`: A function used to filter the consumed components to be returned. The function is invoked for each consumed component with the consumed component as first argument.\n\n**Returns:**\n\nA component iterator.\n\n#### Cloning\n\n##### `clone()` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">possibly async</badge> {#clone-instance-method}\n\nClones the component instance. All primitive attributes are copied, and embedded components are cloned recursively. Currently, identifiable components (i.e., components having an identifier attribute) cannot be cloned, but this might change in the future.\n\n**Returns:**\n\nA clone of the component.\n\n**Example:**\n\n```\nmovie.title = 'Inception';\n\nconst movieClone = movie.clone();\nmovieClone.title = 'Inception 2';\n\nmovieClone.title; // => 'Inception 2'\nmovie.title; // => 'Inception'\n```\n\n##### `clone(value)` <badge type=\"tertiary-outline\">function</badge> <badge type=\"outline\">possibly async</badge> {#clone-function}\n\nDeeply clones any type of values including objects, arrays, and component instances (using Component's [`clone()`](https://layrjs.com/docs/v2/reference/component#clone-instance-method) instance method).\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA clone of the specified value.\n\n**Example:**\n\n```\nimport {clone} from '@layr/component';\n\nconst data = {\n  token: 'xyz123',\n  timestamp: 1596600889609,\n  movie: new Movie({title: 'Inception'})\n};\n\nconst dataClone = clone(data);\ndataClone.token; // => 'xyz123';\ndataClone.timestamp; // => 1596600889609\ndataClone.movie; // => A clone of data.movie\n```\n\n#### Forking\n\n##### `fork()` <badge type=\"secondary\">class method</badge> {#fork-class-method}\n\nCreates a fork of the component class.\n\n**Returns:**\n\nThe component class fork.\n\n**Example:**\n\n```\nclass Movie extends Component {}\n\nMovie.fork(); // => A fork of the `Movie` class\n```\n\n##### `fork()` <badge type=\"secondary-outline\">instance method</badge> {#fork-instance-method}\n\nCreates a fork of the component instance. Note that the constructor of the resulting component will be a fork of the component class.\n\n**Returns:**\n\nThe component instance fork.\n\n**Example:**\n\n```\nclass Movie extends Component {}\nconst movie = new Movie();\n\nmovie.fork(); // => A fork of `movie`\nmovie.fork().constructor.isForkOf(Movie); // => true\n```\n\n##### `isForkOf()` <badge type=\"secondary\">class method</badge> {#is-fork-of-class-method}\n\nReturns whether the component class is a fork of another component class.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nclass Movie extends Component {}\nconst MovieFork = Movie.fork();\n\nMovieFork.isForkOf(Movie); // => true\nMovie.isForkOf(MovieFork); // => false\n```\n\n##### `isForkOf()` <badge type=\"secondary-outline\">instance method</badge> {#is-fork-of-instance-method}\n\nReturns whether the component instance is a fork of another component instance.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nclass Movie extends Component {}\nconst movie = new Movie();\nconst movieFork = movie.fork();\n\nmovieFork.isForkOf(movie); // => true\nmovie.isForkOf(movieFork); // => false\n```\n\n##### `getGhost()` <badge type=\"secondary\">class method</badge> {#get-ghost-class-method}\n\nGets the ghost of the component class. A ghost is like a fork, but it is unique. The first time you call this method, a fork is created, and then, all the successive calls return the same fork.\n\n**Returns:**\n\nThe ghost of the component class.\n\n**Example:**\n\n```\nclass Movie extends Component {}\n\nMovie.getGhost() // => A fork of the `Movie` class\nMovie.getGhost() // => The same fork of the `Movie` class\n```\n\n##### `getGhost()` <badge type=\"secondary-outline\">instance method</badge> {#get-ghost-instance-method}\n\nGets the ghost of the component instance. A ghost is like a fork, but it is unique. The first time you call this method, a fork is created, and then, all the successive calls return the same fork. Only identifiable components (i.e., components having an identifier attribute) can be \"ghosted\".\n\n**Returns:**\n\nThe ghost of the component instance.\n\n**Example:**\n\n```\n// JS\n\nclass Movie extends Component {\n  @primaryIdentifier() id;\n}\n\nconst movie = new Movie();\n\nmovie.getGhost() // => A fork of `movie`\nmovie.getGhost() // => The same fork of `movie`\n```\n```\n// TS\n\nclass Movie extends Component {\n  @primaryIdentifier() id!: string;\n}\n\nconst movie = new Movie();\n\nmovie.getGhost() // => A fork of `movie`\nmovie.getGhost() // => The same fork of `movie`\n```\n\n##### `fork(value)` <badge type=\"tertiary-outline\">function</badge> {#fork-function}\n\nFork any type of values including objects, arrays, and components (using Component's `fork()` [class method](https://layrjs.com/docs/v2/reference/component#fork-class-method) and [instance method](https://layrjs.com/docs/v2/reference/component#fork-instance-method)).\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA fork of the specified value.\n\n**Example:**\n\n```\nimport {fork} from '@layr/component';\n\nconst data = {\n  token: 'xyz123',\n  timestamp: 1596600889609,\n  movie: new Movie({title: 'Inception'})\n};\n\nconst dataFork = fork(data);\nObject.getPrototypeOf(dataFork); // => data\ndataFork.token; // => 'xyz123';\ndataFork.timestamp; // => 1596600889609\ndataFork.movie.isForkOf(data.movie); // => true\n```\n\n#### Merging\n\n##### `merge(componentFork)` <badge type=\"secondary\">class method</badge> {#merge-class-method}\n\nMerges the attributes of a component class fork into the current component class.\n\n**Parameters:**\n\n* `componentFork`: The component class fork to merge.\n\n**Returns:**\n\nThe current component class.\n\n**Example:**\n\n```\nclass Movie extends Component {\n  @attribute('string') static customName = 'Movie';\n}\n\nconst MovieFork = Movie.fork();\nMovieFork.customName = 'Film';\n\nMovie.customName; // => 'Movie'\nMovie.merge(MovieFork);\nMovie.customName; // => 'Film'\n```\n\n##### `merge(componentFork)` <badge type=\"secondary-outline\">instance method</badge> {#merge-instance-method}\n\nMerges the attributes of a component instance fork into the current component instance.\n\n**Parameters:**\n\n* `componentFork`: The component instance fork to merge.\n\n**Returns:**\n\nThe current component instance.\n\n**Example:**\n\n```\nconst movie = new Movie({title: 'Inception'});\nconst movieFork = movie.fork();\nmovieFork.title = 'Inception 2';\n\nmovie.title; // => 'Inception'\nmovie.merge(movieFork);\nmovie.title; // => 'Inception 2'\n```\n\n##### `merge(value, valueFork)` <badge type=\"tertiary-outline\">function</badge> {#merge-function}\n\nDeeply merge any type of forks including objects, arrays, and components (using Component's `merge()` [class method](https://layrjs.com/docs/v2/reference/component#merge-class-method) and [instance method](https://layrjs.com/docs/v2/reference/component#merge-instance-method)) into their original values.\n\n**Parameters:**\n\n* `value`: An original value of any type.\n* `valueFork`: A fork of `value`.\n\n**Returns:**\n\nThe original value.\n\n**Example:**\n\n```\nimport {fork, merge} from '@layr/component';\n\nconst data = {\n  token: 'xyz123',\n  timestamp: 1596600889609,\n  movie: new Movie({title: 'Inception'})\n};\n\nconst dataFork = fork(data);\ndataFork.token = 'xyz456';\ndataFork.movie.title = 'Inception 2';\n\ndata.token; // => 'xyz123'\ndata.movie.title; // => 'Inception'\nmerge(data, dataFork);\ndata.token; // => 'xyz456'\ndata.movie.title; // => 'Inception 2'\n```\n\n#### Serialization\n\n##### `serialize([options])` <badge type=\"secondary\">class method</badge> <badge type=\"outline\">possibly async</badge> {#serialize-class-method}\n\nSerializes the component class to a plain object.\n\n**Parameters:**\n\n* `options`:\n  * `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be serialized (default: `true`, which means that all the attributes will be serialized).\n  * `attributeFilter`: A (possibly async) function used to filter the attributes to be serialized. The function is invoked for each attribute with an [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) instance as first argument.\n  * `target`: A string specifying the [target](https://layrjs.com/docs/v2/reference/attribute#value-source-type) of the serialization (default: `undefined`).\n\n**Returns:**\n\nA plain object representing the serialized component class.\n\n**Example:**\n\n```\nclass Movie extends Component {\n  @attribute('string') static customName = 'Film';\n}\n\nMovie.serialize(); // => {__component: 'typeof Movie', customName: 'Film'}\n```\n\n##### `serialize([options])` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">possibly async</badge> {#serialize-instance-method}\n\nSerializes the component instance to a plain object.\n\n**Parameters:**\n\n* `options`:\n  * `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be serialized (default: `true`, which means that all the attributes will be serialized).\n  * `attributeFilter`: A (possibly async) function used to filter the attributes to be serialized. The function is invoked for each attribute with an [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) instance as first argument.\n  * `target`: A string specifying the [target](https://layrjs.com/docs/v2/reference/attribute#value-source-type) of the serialization (default: `undefined`).\n\n**Returns:**\n\nA plain object representing the serialized component instance.\n\n**Example:**\n\n```\nconst movie = new Movie({title: 'Inception'});\n\nmovie.serialize(); // => {__component: 'Movie', title: 'Inception'}\n```\n\n##### `serialize(value, [options])` <badge type=\"tertiary-outline\">function</badge> <badge type=\"outline\">possibly async</badge> {#serialize-function}\n\nSerializes any type of values including objects, arrays, dates, and components (using Component's `serialize()` [class method](https://layrjs.com/docs/v2/reference/component#serialize-class-method) and [instance method](https://layrjs.com/docs/v2/reference/component#serialize-instance-method)).\n\n**Parameters:**\n\n* `value`: A value of any type.\n* `options`:\n  * `attributeFilter`: A (possibly async) function used to filter the component attributes to be serialized. The function is invoked for each attribute with an [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) instance as first argument.\n  * `target`: The target of the serialization (default: `undefined`).\n\n**Returns:**\n\nThe serialized value.\n\n**Example:**\n\n```\nimport {serialize} from '@layr/component';\n\nconst data = {\n  createdOn: new Date(),\n  updatedOn: undefined,\n  movie: new Movie({title: 'Inception'})\n};\n\nconsole.log(serialize(data));\n\n// Should output something like:\n// {\n//   createdOn: {__date: \"2020-07-18T23:43:33.778Z\"},\n//   updatedOn: {__undefined: true},\n//   movie: {__component: 'Movie', title: 'Inception'}\n// }\n```\n\n#### Deserialization\n\n##### `deserialize([object], [options])` <badge type=\"secondary\">class method</badge> <badge type=\"outline\">possibly async</badge> {#deserialize-class-method}\n\nDeserializes the component class from the specified plain object. The deserialization operates \"in place\", which means that the current component class attributes are mutated.\n\n**Parameters:**\n\n* `object`: The plain object to deserialize from.\n* `options`:\n  * `attributeFilter`: A (possibly async) function used to filter the attributes to be deserialized. The function is invoked for each attribute with an [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) instance as first argument.\n  * `source`: A string specifying the [source](https://layrjs.com/docs/v2/reference/attribute#value-source-type) of the serialization (default: `'local'`).\n\n**Returns:**\n\nThe component class.\n\n**Example:**\n\n```\nclass Movie extends Component {\n  @attribute('string') static customName = 'Movie';\n}\n\nMovie.customName; // => 'Movie'\nMovie.deserialize({customName: 'Film'});\nMovie.customName; // => 'Film'\n```\n\n##### `deserialize([object], [options])` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">possibly async</badge> {#deserialize-instance-method}\n\nDeserializes the component instance from the specified plain object. The deserialization operates \"in place\", which means that the current component instance attributes are mutated.\n\n**Parameters:**\n\n* `object`: The plain object to deserialize from.\n* `options`:\n  * `attributeFilter`: A (possibly async) function used to filter the attributes to be deserialized. The function is invoked for each attribute with an [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) instance as first argument.\n  * `source`: A string specifying the [source](https://layrjs.com/docs/v2/reference/attribute#value-source-type) of the serialization (default: `'local'`).\n\n**Returns:**\n\nThe current component instance.\n\n**Example:**\n\n```\nclass Movie extends Component {\n  @attribute('string') title = '';\n}\n\nconst movie = new Movie();\n\nmovie.title; // => ''\nmovie.deserialize({title: 'Inception'});\nmovie.title; // => 'Inception'\n```\n\n##### `deserialize(value, [options])` <badge type=\"tertiary-outline\">function</badge> <badge type=\"outline\">possibly async</badge> {#deserialize-function}\n\nDeserializes any type of serialized values including objects, arrays, dates, and components.\n\n**Parameters:**\n\n* `value`: A serialized value.\n* `options`:\n  * `rootComponent`: The root component of your app.\n  * `attributeFilter`: A (possibly async) function used to filter the component attributes to be deserialized. The function is invoked for each attribute with an [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) instance as first argument.\n  * `source`: The source of the serialization (default: `'local'`).\n\n**Returns:**\n\nThe deserialized value.\n\n**Example:**\n\n```\n// JS\n\nimport {Component, deserialize} from '@layr/component';\n\nclass Movie extends Component {\n  @attribute('string') title;\n}\n\nconst serializedData = {\n  createdOn: {__date: \"2020-07-18T23:43:33.778Z\"},\n  updatedOn: {__undefined: true},\n  movie: {__component: 'Movie', title: 'Inception'}\n};\n\nconst data = deserialize(serializedData, {rootComponent: Movie});\n\ndata.createdOn; // => A Date instance\ndata.updatedOn; // => undefined\ndata.movie; // => A Movie instance\n```\n```\n// TS\n\nimport {Component, deserialize} from '@layr/component';\n\nclass Movie extends Component {\n  @attribute('string') title!: string;\n}\n\nconst serializedData = {\n  createdOn: {__date: \"2020-07-18T23:43:33.778Z\"},\n  updatedOn: {__undefined: true},\n  movie: {__component: 'Movie', title: 'Inception'}\n};\n\nconst data = deserialize(serializedData, {rootComponent: Movie});\n\ndata.createdOn; // => A Date instance\ndata.updatedOn; // => undefined\ndata.movie; // => A Movie instance\n```\n\n#### Decorators\n\n##### `@attribute([valueType], [options])` <badge type=\"tertiary\">decorator</badge> {#attribute-decorator}\n\nDecorates an attribute of a component so it can be type checked at runtime, validated, serialized, observed, etc.\n\n**Parameters:**\n\n* `valueType`: A string specifying the [type of values](https://layrjs.com/docs/v2/reference/value-type#supported-types) that can be stored in the attribute (default: `'any'`).\n* `options`: The options to create the [`Attribute`](https://layrjs.com/docs/v2/reference/attribute#constructor).\n\n**Example:**\n\n```\n// JS\n\nimport {Component, attribute, validators} from '@layr/component';\n\nconst {maxLength} = validators;\n\nclass Movie extends Component {\n  // Optional 'string' class attribute\n  @attribute('string?') static customName;\n\n  // Required 'string' instance attribute\n  @attribute('string') title;\n\n  // Optional 'string' instance attribute with a validator\n  @attribute('string?', {validators: [maxLength(100)]}) summary;\n\n  // Required array of 'Actor' instance attribute with a default value\n  @attribute('Actor[]') actors = [];\n}\n```\n```\n// TS\n\nimport {Component, attribute, validators} from '@layr/component';\n\nconst {maxLength} = validators;\n\nclass Movie extends Component {\n  // Optional 'string' class attribute\n  @attribute('string?') static customName?: string;\n\n  // Required 'string' instance attribute\n  @attribute('string') title!: string;\n\n  // Optional 'string' instance attribute with a validator\n  @attribute('string?', {validators: [maxLength(100)]}) summary?: string;\n\n  // Required array of 'Actor' instance attribute with a default value\n  @attribute('Actor[]') actors: Actor[] = [];\n}\n```\n\n##### `@primaryIdentifier([valueType], [options])` <badge type=\"tertiary\">decorator</badge> {#primary-identifier-decorator}\n\nDecorates an attribute of a component as a [primary identifier attribute](https://layrjs.com/docs/v2/reference/primary-identifier-attribute).\n\n**Parameters:**\n\n* `valueType`: A string specifying the type of values the attribute can store. It can be either `'string'` or `'number'` (default: `'string'`).\n* `options`: The options to create the [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute).\n\n**Example:**\n\n```\n// JS\n\nimport {Component, primaryIdentifier} from '@layr/component';\n\nclass Movie extends Component {\n  // Auto-generated 'string' primary identifier attribute\n  @primaryIdentifier('string') id;\n}\n\nclass Film extends Component {\n  // Custom 'number' primary identifier attribute\n  @primaryIdentifier('number', {default() { return Math.random(); }}) id;\n}\n```\n```\n// TS\n\nimport {Component, primaryIdentifier} from '@layr/component';\n\nclass Movie extends Component {\n  // Auto-generated 'string' primary identifier attribute\n  @primaryIdentifier('string') id!: string;\n}\n\nclass Film extends Component {\n  // Custom 'number' primary identifier attribute\n  @primaryIdentifier('number', {default() { return Math.random(); }}) id!: number;\n}\n```\n\n##### `@secondaryIdentifier([valueType], [options])` <badge type=\"tertiary\">decorator</badge> {#secondary-identifier-decorator}\n\nDecorates an attribute of a component as a [secondary identifier attribute](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute).\n\n**Parameters:**\n\n* `valueType`: A string specifying the type of values the attribute can store. It can be either `'string'` or `'number'` (default: `'string'`).\n* `options`: The options to create the [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute).\n\n**Example:**\n\n```\n// JS\n\nimport {Component, secondaryIdentifier} from '@layr/component';\n\nclass Movie extends Component {\n  // 'string' secondary identifier attribute\n  @secondaryIdentifier('string') slug;\n\n  // 'number' secondary identifier attribute\n  @secondaryIdentifier('number') reference;\n}\n```\n```\n// TS\n\nimport {Component, secondaryIdentifier} from '@layr/component';\n\nclass Movie extends Component {\n  // 'string' secondary identifier attribute\n  @secondaryIdentifier('string') slug!: string;\n\n  // 'number' secondary identifier attribute\n  @secondaryIdentifier('number') reference!: number;\n}\n```\n\n##### `@method([options])` <badge type=\"tertiary\">decorator</badge> {#method-decorator}\n\nDecorates a method of a component so it can be exposed and called remotely.\n\n**Parameters:**\n\n* `options`: The options to create the [`Method`](https://layrjs.com/docs/v2/reference/method#constructor).\n\n**Example:**\n\n```\nimport {Component, method} from '@layr/component';\n\nclass Movie extends Component {\n  // Class method\n  @method() static getConfig() {\n    // ...\n  }\n\n  // Instance method\n  @method() play() {\n    // ...\n  }\n}\n```\n\n##### `@expose(exposure)` <badge type=\"tertiary\">decorator</badge> {#expose-decorator}\n\nExposes some attributes or methods of a component so they can be consumed remotely.\n\nThis decorator is usually placed before a component attribute or method, but it can also be placed before a component class. When placed before a component class, you can expose several attributes or methods at once, and even better, you can expose attributes or methods that are defined in a parent class.\n\n**Parameters:**\n\n* `exposure`: An object specifying which operations should be exposed. When the decorator is placed before a component attribute or method, the object is of type [`PropertyExposure`](https://layrjs.com/docs/v2/reference/property#property-exposure-type). When the decorator is placed before a component class, the shape of the object is `{[propertyName]: PropertyExposure, prototype: {[propertyName]: PropertyExposure}}`.\n\n**Example:**\n\n```\n// JS\n\nimport {Component, expose, attribute, method} from '@layr/component';\n\nclass Movie extends Component {\n  // Class attribute exposing the 'get' operation only\n  @expose({get: true}) @attribute('string?') static customName;\n\n  // Instance attribute exposing the 'get' and 'set' operations\n  @expose({get: true, set: true}) @attribute('string') title;\n\n  // Class method exposure\n  @expose({call: true}) @method() static getConfig() {\n    // ...\n  }\n\n  // Instance method exposure\n  @expose({call: true}) @method() play() {\n    // ...\n  }\n}\n\n// Exposing some class and instance methods that are defined in a parent class\n@expose({find: {call: true}, prototype: {load: {call: true}}})\nclass Actor extends Storable(Component) {\n  // ...\n}\n```\n```\n// TS\n\nimport {Component, expose, attribute, method} from '@layr/component';\n\nclass Movie extends Component {\n  // Class attribute exposing the 'get' operation only\n  @expose({get: true}) @attribute('string?') static customName?: string;\n\n  // Instance attribute exposing the 'get' and 'set' operations\n  @expose({get: true, set: true}) @attribute('string') title!: string;\n\n  // Class method exposure\n  @expose({call: true}) @method() static getConfig() {\n    // ...\n  }\n\n  // Instance method exposure\n  @expose({call: true}) @method() play() {\n    // ...\n  }\n}\n\n// Exposing some class and instance methods that are defined in a parent class\n@expose({find: {call: true}, prototype: {load: {call: true}}})\nclass Actor extends Storable(Component) {\n  // ...\n}\n```\n\n##### `@provide()` <badge type=\"tertiary\">decorator</badge> {#provide-decorator}\n\nProvides a component so it can be easily accessed from the current component or from any component that is \"consuming\" it using the [`@consume()`](https://layrjs.com/docs/v2/reference/component#consume-decorator) decorator.\n\n**Example:**\n\n```\n// JS\n\nimport {Component, provide, consume} from '@layr/component';\n\nclass Movie extends Component {\n  @consume() static Actor;\n}\n\nclass Actor extends Component {}\n\nclass Application extends Component {\n  @provide() static Movie = Movie;\n  @provide() static Actor = Actor;\n}\n\n// Since `Actor` is provided by `Application`, it can be accessed from `Movie`\nMovie.Actor; // => Actor\n```\n```\n// TS\n\nimport {Component, provide, consume} from '@layr/component';\n\nclass Movie extends Component {\n  @consume() static Actor: typeof Actor;\n}\n\nclass Actor extends Component {}\n\nclass Application extends Component {\n  @provide() static Movie = Movie;\n  @provide() static Actor = Actor;\n}\n\n// Since `Actor` is provided by `Application`, it can be accessed from `Movie`\nMovie.Actor; // => Actor\n```\n\n##### `@consume()` <badge type=\"tertiary\">decorator</badge> {#consume-decorator}\n\nConsumes a component provided by the provider (or recursively, any provider's provider) of the current component so it can be easily accessed using a component accessor.\n\n**Example:**\n\nSee [`@provide()`'s example](https://layrjs.com/docs/v2/reference/component#provide-decorator).\n#### Utilities\n\n##### `isComponentClass(value)` <badge type=\"tertiary-outline\">function</badge> {#is-component-class-function}\n\nReturns whether the specified value is a component class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isComponentInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-component-instance-function}\n\nReturns whether the specified value is a component instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isComponentClassOrInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-component-class-or-instance-function}\n\nReturns whether the specified value is a component class or instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `assertIsComponentClass(value)` <badge type=\"tertiary-outline\">function</badge> {#assert-is-component-class-function}\n\nThrows an error if the specified value is not a component class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n##### `assertIsComponentInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#assert-is-component-instance-function}\n\nThrows an error if the specified value is not a component instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n##### `assertIsComponentClassOrInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#assert-is-component-class-or-instance-function}\n\nThrows an error if the specified value is not a component class or instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n##### `ensureComponentClass(component)` <badge type=\"tertiary-outline\">function</badge> {#ensure-component-class-function}\n\nEnsures that the specified component is a class. If you specify a component instance (or prototype), the class of the component is returned. If you specify a component class, it is returned as is.\n\n**Parameters:**\n\n* `component`: A component class or instance.\n\n**Returns:**\n\nA component class.\n\n**Example:**\n\n```\nensureComponentClass(movie) => Movie\nensureComponentClass(Movie.prototype) => Movie\nensureComponentClass(Movie) => Movie\n```\n\n##### `ensureComponentInstance(component)` <badge type=\"tertiary-outline\">function</badge> {#ensure-component-instance-function}\n\nEnsures that the specified component is an instance (or prototype). If you specify a component class, the component prototype is returned. If you specify a component instance (or prototype), it is returned as is.\n\n**Parameters:**\n\n* `component`: A component class or instance.\n\n**Returns:**\n\nA component instance (or prototype).\n\n**Example:**\n\n```\nensureComponentInstance(Movie) => Movie.prototype\nensureComponentInstance(Movie.prototype) => Movie.prototype\nensureComponentInstance(movie) => movie\n```\n\n##### `isComponentName(name)` <badge type=\"tertiary-outline\">function</badge> {#is-component-name-function}\n\nReturns whether the specified string is a valid component name. The rule is the same as for typical JavaScript class names.\n\n**Parameters:**\n\n* `name`: The string to check.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nisComponentName('Movie') => true\nisComponentName('Movie123') => true\nisComponentName('Awesome_Movie') => true\nisComponentName('123Movie') => false\nisComponentName('Awesome-Movie') => false\nisComponentName('movie') => false\n```\n\n##### `assertIsComponentName(name)` <badge type=\"tertiary-outline\">function</badge> {#assert-is-component-name-function}\n\nThrows an error if the specified string is not a valid component name.\n\n**Parameters:**\n\n* `name`: The string to check.\n\n##### `getComponentNameFromComponentClassType(name)` <badge type=\"tertiary-outline\">function</badge> {#get-component-name-from-component-class-type-function}\n\nTransforms a component class type into a component name.\n\n**Parameters:**\n\n* `name`: A string representing a component class type.\n\n**Returns:**\n\nA component name.\n\n**Example:**\n\n```\ngetComponentNameFromComponentClassType('typeof Movie') => 'Movie'\n```\n\n##### `getComponentNameFromComponentInstanceType(name)` <badge type=\"tertiary-outline\">function</badge> {#get-component-name-from-component-instance-type-function}\n\nTransforms a component instance type into a component name.\n\n**Parameters:**\n\n* `name`: A string representing a component instance type.\n\n**Returns:**\n\nA component name.\n\n**Example:**\n\n```\ngetComponentNameFromComponentInstanceType('Movie') => 'Movie'\n```\n\n##### `isComponentType(name, [options])` <badge type=\"tertiary-outline\">function</badge> {#is-component-type-function}\n\nReturns whether the specified string is a valid component type.\n\n**Parameters:**\n\n* `name`: The string to check.\n* `options`:\n  * `allowClasses`: A boolean specifying whether component class types are allowed (default: `true`).\n  * `allowInstances`: A boolean specifying whether component instance types are allowed (default: `true`).\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nisComponentType('typeof Movie') => true\nisComponentType('Movie') => true\nisComponentType('typeof Awesome-Movie') => false\nisComponentType('movie') => false\nisComponentType('typeof Movie', {allowClasses: false}) => false\nisComponentType('Movie', {allowInstances: false}) => false\n```\n\n##### `assertIsComponentType(name, [options])` <badge type=\"tertiary-outline\">function</badge> {#assert-is-component-type-function}\n\nThrows an error if the specified string is not a valid component type.\n\n**Parameters:**\n\n* `name`: The string to check.\n* `options`:\n  * `allowClasses`: A boolean specifying whether component class types are allowed (default: `true`).\n  * `allowInstances`: A boolean specifying whether component instance types are allowed (default: `true`).\n\n##### `getComponentClassTypeFromComponentName(name)` <badge type=\"tertiary-outline\">function</badge> {#get-component-class-type-from-component-name-function}\n\nTransforms a component name into a component class type.\n\n**Parameters:**\n\n* `name`: A component name.\n\n**Returns:**\n\nA component class type.\n\n**Example:**\n\n```\ngetComponentClassTypeFromComponentName('Movie') => 'typeof Movie'\n```\n\n##### `getComponentInstanceTypeFromComponentName(name)` <badge type=\"tertiary-outline\">function</badge> {#get-component-instance-type-from-component-name-function}\n\nTransforms a component name into a component instance type.\n\n**Parameters:**\n\n* `name`: A component name.\n\n**Returns:**\n\nA component instance type.\n\n**Example:**\n\n```\ngetComponentInstanceTypeFromComponentName('Movie') => 'Movie'\n```\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/component-client-69HLk6gKzg5DfWcVC2bQWS.immutable.md",
    "content": "### ComponentClient <badge type=\"primary\">class</badge> {#component-client-class}\n\nA base class allowing to access a root [`Component`](https://layrjs.com/docs/v2/reference/component) that is served by a [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server).\n\nTypically, instead of using this class, you would use a subclass such as [`ComponentHTTPClient`](https://layrjs.com/docs/v2/reference/component-http-client).\n\n#### Creation\n\n##### `new ComponentClient(componentServer, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates a component client.\n\n**Parameters:**\n\n* `componentServer`: The [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server) to connect to.\n* `options`:\n  * `version`: A number specifying the expected version of the component server (default: `undefined`). If a version is specified, an error is thrown when a request is sent and the component server has a different version. The thrown error is a JavaScript `Error` instance with a `code` attribute set to `'COMPONENT_CLIENT_VERSION_DOES_NOT_MATCH_COMPONENT_SERVER_VERSION'`.\n  * `mixins`: An array of the component mixins (e.g., [`Storable`](https://layrjs.com/docs/v2/reference/storable)) to use when constructing the components exposed by the component server (default: `[]`).\n\n**Returns:**\n\nA `ComponentClient` instance.\n\n**Example:**\n\n```\n// JS\n\nimport {Component, attribute, expose} from '@layr/component';\nimport {ComponentClient} from '@layr/component-client';\nimport {ComponentServer} from '@layr/component-server';\n\nclass Movie extends Component {\n  @expose({get: true, set: true}) @attribute('string') title;\n}\n\nconst server = new ComponentServer(Movie);\nconst client = new ComponentClient(server);\n\nconst RemoteMovie = client.getComponent();\n```\n```\n// TS\n\nimport {Component, attribute, expose} from '@layr/component';\nimport {ComponentClient} from '@layr/component-client';\nimport {ComponentServer} from '@layr/component-server';\n\nclass Movie extends Component {\n  @expose({get: true, set: true}) @attribute('string') title!: string;\n}\n\nconst server = new ComponentServer(Movie);\nconst client = new ComponentClient(server);\n\nconst RemoteMovie = client.getComponent() as typeof Movie;\n```\n\n#### Getting the Served Component\n\n##### `getComponent()` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">possibly async</badge> {#get-component-instance-method}\n\nGets the component that is served by the component server.\n\n**Returns:**\n\nA [`Component`](https://layrjs.com/docs/v2/reference/component) class.\n\n**Example:**\n\nSee [`constructor`'s example](https://layrjs.com/docs/v2/reference/component-client#constructor)."
  },
  {
    "path": "website/frontend/public/docs/v2/reference/component-express-middleware-7KGBerWY9jB9r9eOjwLmPq.immutable.md",
    "content": "### component-express-middleware <badge type=\"primary\">module</badge> {#component-express-middleware-module}\n\nAn [Express](https://expressjs.com/) middleware allowing to serve a root [`Component`](https://layrjs.com/docs/v2/reference/component) so it can be accessed by a [`ComponentHTTPClient`](https://layrjs.com/docs/v2/reference/component-http-client).\n\n#### Usage\n\nCall the [`serveComponent()`](https://layrjs.com/docs/v2/reference/component-express-middleware#serve-component-function) function to create a middleware for your Express app.\n\n**Example:**\n\n```\nimport express from 'express';\nimport {Component} from '@layr/component';\nimport {serveComponent} from '@layr/component-express-middleware';\n\nclass Movie extends Component {\n  // ...\n}\n\nconst app = express();\n\napp.use('/api', serveComponent(Movie));\n\napp.listen(3210);\n```\n\n#### Functions\n\n##### `serveComponent(componentOrComponentServer, [options])` <badge type=\"tertiary-outline\">function</badge> {#serve-component-function}\n\nCreates an [Express](https://expressjs.com/) middleware exposing the specified root [`Component`](https://layrjs.com/docs/v2/reference/component) class.\n\n**Parameters:**\n\n* `componentOrComponentServer`: The root [`Component`](https://layrjs.com/docs/v2/reference/component) class to serve. An instance of a [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server) will be created under the hood. Alternatively, you can pass an existing instance of a [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server).\n* `options`:\n  * `version`: A number specifying the version of the created [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server) (default: `undefined`).\n\n**Returns:**\n\nAn Express middleware.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/component-http-client-2rHYFMcsawI16EvGk8372w.immutable.md",
    "content": "### ComponentHTTPClient <badge type=\"primary\">class</badge> {#component-http-client-class}\n\n*Inherits from [`ComponentClient`](https://layrjs.com/docs/v2/reference/component-client).*\n\nA class allowing to access a root [`Component`](https://layrjs.com/docs/v2/reference/component) that is served by a [`ComponentHTTPServer`](https://layrjs.com/docs/v2/reference/component-http-server), a middleware such as [`component-express-middleware`](https://layrjs.com/docs/v2/reference/component-express-middleware), or any HTTP server exposing a [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server).\n\n#### Usage\n\nCreate an instance of `ComponentHTTPClient` by specifying the URL of the component server, and use the [`getComponent()`](https://layrjs.com/docs/v2/reference/component-http-client#get-component-instance-method) method to get the served component.\n\nFor example, to access a `Movie` component that is served by a component server, you could do the following:\n\n```\n// JS\n\n// backend.js\n\nimport {Component, attribute, method, expose} from '@layr/component';\nimport {ComponentHTTPServer} from '@layr/component-http-server';\n\nexport class Movie extends Component {\n  @expose({get: true, set: true}) @attribute('string') title;\n\n  @expose({call: true}) @method() async play() {\n    return `Playing `${this.title}`...`;\n  }\n}\n\nconst server = new ComponentHTTPServer(Movie, {port: 3210});\n\nserver.start();\n```\n\n```\n// JS\n\n// frontend.js\n\nimport {ComponentHTTPClient} from '@layr/component-http-client';\n\n(async () => {\n  const client = new ComponentHTTPClient('http://localhost:3210');\n\n  const Movie = await client.getComponent();\n\n  const movie = new Movie({title: 'Inception'});\n\n  await movie.play(); // => 'Playing Inception...'\n})();\n```\n\n```\n// TS\n\n// backend.ts\n\nimport {Component, attribute, method, expose} from '@layr/component';\nimport {ComponentHTTPServer} from '@layr/component-http-server';\n\nexport class Movie extends Component {\n  @expose({get: true, set: true}) @attribute('string') title!: string;\n\n  @expose({call: true}) @method() async play() {\n    return `Playing `${this.title}`...`;\n  }\n}\n\nconst server = new ComponentHTTPServer(Movie, {port: 3210});\n\nserver.start();\n```\n\n```\n// TS\n\n// frontend.ts\n\nimport {ComponentHTTPClient} from '@layr/component-http-client';\n\nimport type {Movie as MovieType} from './backend';\n\n(async () => {\n  const client = new ComponentHTTPClient('http://localhost:3210');\n\n  const Movie = (await client.getComponent()) as typeof MovieType;\n\n  const movie = new Movie({title: 'Inception'});\n\n  await movie.play(); // => 'Playing Inception...'\n})();\n```\n\n#### Creation\n\n##### `new ComponentHTTPClient(url, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates a component HTTP client.\n\n**Parameters:**\n\n* `url`: A string specifying the URL of the component server to connect to.\n* `options`:\n  * `version`: A number specifying the expected version of the component server (default: `undefined`). If a version is specified, an error is thrown when a request is sent and the component server has a different version. The thrown error is a JavaScript `Error` instance with a `code` attribute set to `'COMPONENT_CLIENT_VERSION_DOES_NOT_MATCH_COMPONENT_SERVER_VERSION'`.\n  * `mixins`: An array of the component mixins (e.g., [`Storable`](https://layrjs.com/docs/v2/reference/storable)) to use when constructing the components exposed by the component server (default: `[]`).\n  * `retryFailedRequests`: A boolean or a function returning a boolean specifying whether a request should be retried in case of a network issue (default: `false`). In case a function is specified, the function will receive an object of the shape `{error, numberOfRetries}` where `error` is the error that has occurred and `numberOfRetries` is the number of retries that has been attempted so far. The function can be asynchronous ans should return a boolean.\n  * `maximumRequestRetries`: The maximum number of times a request can be retried (default: `10`).\n  * `minimumTimeBetweenRequestRetries`: A number specifying the minimum time in milliseconds that should elapse between each request retry (default: `3000`).\n\n**Returns:**\n\nA `ComponentHTTPClient` instance.\n\n#### Getting the Served Component\n\n##### `getComponent()` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#get-component-instance-method}\n\nGets the component that is served by the component server.\n\n**Returns:**\n\nA [`Component`](https://layrjs.com/docs/v2/reference/component) class.\n\n**Example:**\n\nSee an [example of use](https://layrjs.com/docs/v2/reference/component-http-client#usage) above."
  },
  {
    "path": "website/frontend/public/docs/v2/reference/component-http-server-5aWTLSzVvm0WohfP6af7OP.immutable.md",
    "content": "### ComponentHTTPServer <badge type=\"primary\">class</badge> {#component-http-server-class}\n\nA class allowing to serve a root [`Component`](https://layrjs.com/docs/v2/reference/component) so it can be accessed by a [`ComponentHTTPClient`](https://layrjs.com/docs/v2/reference/component-http-client).\n\nThis class provides a basic HTTP server providing one endpoint to serve your root component. If you wish to build an HTTP server providing multiple endpoints, you can use a middleware such as [`component-express-middleware`](https://layrjs.com/docs/v2/reference/component-express-middleware), or implement the necessary plumbing to integrate a [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server) in your custom HTTP server.\n\n#### Usage\n\nCreate an instance of `ComponentHTTPServer` by specifying the root [`Component`](https://layrjs.com/docs/v2/reference/component) you want to serve, and use the [`start()`](https://layrjs.com/docs/v2/reference/component-http-server#start-instance-method) method to start the server.\n\nSee an example of use in [`ComponentHTTPClient`](https://layrjs.com/docs/v2/reference/component-http-client).\n\n#### Creation\n\n##### `new ComponentHTTPServer(componentOrComponentServer, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates a component HTTP server.\n\n**Parameters:**\n\n* `componentOrComponentServer`: The root [`Component`](https://layrjs.com/docs/v2/reference/component) class to serve. An instance of a [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server) will be created under the hood. Alternatively, you can pass an existing instance of a [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server).\n* `options`:\n  * `port`: A number specifying the TCP port to listen to (default: `3333`).\n  * `version`: A number specifying the version of the created [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server) (default: `undefined`).\n\n**Returns:**\n\nA `ComponentHTTPServer` instance.\n\n#### Methods\n\n##### `start()` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#start-instance-method}\n\nStarts the component HTTP server.\n\n**Example:**\n\n```\nconst server = new ComponentHTTPServer(Movie, {port: 3210});\n\nawait server.start();\n```\n\n##### `stop()` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#stop-instance-method}\n\nStops the component HTTP server.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/component-koa-middleware-6aVaQsSgkzWt35XyqSMuqx.immutable.md",
    "content": "### component-koa-middleware <badge type=\"primary\">module</badge> {#component-koa-middleware-module}\n\nA [Koa](https://koajs.com/) middleware allowing to serve a root [`Component`](https://layrjs.com/docs/v2/reference/component) so it can be accessed by a [`ComponentHTTPClient`](https://layrjs.com/docs/v2/reference/component-http-client).\n\n#### Usage\n\nCall the [`serveComponent()`](https://layrjs.com/docs/v2/reference/component-koa-middleware#serve-component-function) function to create a middleware for your Koa app.\n\n**Example:**\n\n```\nimport Koa from 'koa';\nimport {Component} from '@layr/component';\nimport {serveComponent} from '@layr/component-koa-middleware';\n\nclass Movie extends Component {\n  // ...\n}\n\nconst app = new Koa();\n\n// Serve the `Movie` component at the root ('/')\napp.use(serveComponent(Movie));\n\napp.listen(3210);\n```\n\nIf you want to serve your component at a specific URL, you can use [`koa-mount`](https://github.com/koajs/mount):\n\n```\nimport mount from 'koa-mount';\n\n// Serve the `Movie` component at a specific URL ('/api')\napp.use(mount('/api', serveComponent(Movie)));\n```\n\n#### Functions\n\n##### `serveComponent(componentOrComponentServer, [options])` <badge type=\"tertiary-outline\">function</badge> {#serve-component-function}\n\nCreates a [Koa](https://koajs.com/) middleware exposing the specified root [`Component`](https://layrjs.com/docs/v2/reference/component) class.\n\n**Parameters:**\n\n* `componentOrComponentServer`: The root [`Component`](https://layrjs.com/docs/v2/reference/component) class to serve. An instance of a [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server) will be created under the hood. Alternatively, you can pass an existing instance of a [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server).\n* `options`:\n  * `version`: A number specifying the version of the created [`ComponentServer`](https://layrjs.com/docs/v2/reference/component-server) (default: `undefined`).\n\n**Returns:**\n\nA Koa middleware.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/component-server-i0AsOVFbGCJUrJ9mJyjPh.immutable.md",
    "content": "### ComponentServer <badge type=\"primary\">class</badge> {#component-server-class}\n\nA base class allowing to serve a root [`Component`](https://layrjs.com/docs/v2/reference/component) so it can be accessed by a [`ComponentClient`](https://layrjs.com/docs/v2/reference/component-client).\n\nTypically, instead of using this class, you would use a class such as [`ComponentHTTPServer`](https://layrjs.com/docs/v2/reference/component-http-server), or a middleware such as [`component-express-middleware`](https://layrjs.com/docs/v2/reference/component-express-middleware).\n\n#### Creation\n\n##### `new ComponentServer(component, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates a component server.\n\n**Parameters:**\n\n* `component`: The root [`Component`](https://layrjs.com/docs/v2/reference/component) class to serve.\n* `options`:\n  * `version`: A number specifying the version of the returned component server (default: `undefined`).\n\n**Returns:**\n\nA `ComponentServer` instance.\n\n**Example:**\n\nSee [`ComponentClient`'s example](https://layrjs.com/docs/v2/reference/component-client#constructor)."
  },
  {
    "path": "website/frontend/public/docs/v2/reference/embedded-component-3YdGqfLNxl8c001AvWxHZ6.immutable.md",
    "content": "### EmbeddedComponent <badge type=\"primary\">class</badge> {#embedded-component-class}\n\n*Inherits from [`Component`](https://layrjs.com/docs/v2/reference/component).*\n\nThe `EmbeddedComponent` class allows you to define a component that can be embedded into another component. This is useful when you have to deal with a rich data model composed of a hierarchy of properties that can be type checked at runtime and validated. If you don't need such control over some nested attributes, instead of using an embedded component, you can just use an attribute of type `object`.\n\nThe `EmbeddedComponent` class inherits from the [`Component`](https://layrjs.com/docs/v2/reference/component) class, so you can define and consume an embedded component in the same way you would do with any component.\n\nHowever, since an embedded component is owned by its parent component, it doesn't behave like a regular component. Head over [here](https://layrjs.com/docs/v2/reference/component#nesting-components) for a broader explanation.\n\n#### Usage\n\nJust extend the `EmbeddedComponent` class to define a component that has the ability to be embedded.\n\nFor example, a `MovieDetails` embedded component could be defined as follows:\n\n```\n// JS\n\n// movie-details.js\n\nimport {EmbeddedComponent} from '@layr/component';\n\nexport class MovieDetails extends EmbeddedComponent {\n  @attribute('number?') duration;\n  @attribute('string?') aspectRatio;\n}\n```\n\n```\n// TS\n\n// movie-details.ts\n\nimport {EmbeddedComponent} from '@layr/component';\n\nexport class MovieDetails extends EmbeddedComponent {\n  @attribute('number?') duration?: number;\n  @attribute('string?') aspectRatio?: string;\n}\n```\n\nOnce you have defined an embedded component, you can embed it into any other component (a regular component or even another embedded component). For example, here is a `Movie` component that is embedding the `MovieDetails` component:\n\n```\n// JS\n\n// movie.js\n\nimport {Component} from '@layr/component';\n\nimport {MovieDetails} from './movie-details';\n\nclass Movie extends Component {\n  @provide() static MovieDetails = MovieDetails;\n\n  @attribute('string') title;\n  @attribute('MovieDetails') details;\n}\n```\n\n```\n// TS\n\n// movie.ts\n\nimport {Component} from '@layr/component';\n\nimport {MovieDetails} from './movie-details';\n\nclass Movie extends Component {\n  @provide() static MovieDetails = MovieDetails;\n\n  @attribute('string') title!: string;\n  @attribute('MovieDetails') details!: MovieDetails;\n}\n```\n\n> Note that you have to make the `MovieDetails` component accessible from the `Movie` component by using the [`@provide()`](https://layrjs.com/docs/v2/reference/component#provide-decorator) decorator. This way, the `MovieDetails` component can be later referred by its name when you define the `details` attribute using the [`@attribute()`](https://layrjs.com/docs/v2/reference/component#attribute-decorator) decorator.\n\nFinally, the `Movie` component can be instantiated like this:\n\n```\nconst movie = new Movie({\n  title: 'Inception',\n  details: new Movie.MovieDetails({duration: 120, aspectRatio: '16:9'})\n});\n\nmovie.title; // => 'Inception'\nmovie.details.duration; // => 120\n```\n\n#### Methods\n\nSee the methods that are inherited from the [`Component`](https://layrjs.com/docs/v2/reference/component#creation) class.\n\n#### Observability\n\nSee the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) class.\n\n#### Embeddability\n\n##### `isEmbedded()` <badge type=\"secondary\">class method</badge> {#is-embedded-class-method}\n\nAlways returns `true`.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/identifier-attribute-2w8hckqCKGHPurgIGY5vhT.immutable.md",
    "content": "### IdentifierAttribute <badge type=\"primary\">class</badge> {#identifier-attribute-class}\n\n*Inherits from [`Attribute`](https://layrjs.com/docs/v2/reference/attribute).*\n\nA base class from which [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute) and [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute) are constructed. Unless you build a custom identifier attribute class, you probably won't have to use this class directly.\n\n#### Utilities\n\n##### `isIdentifierAttributeClass(value)` <badge type=\"tertiary-outline\">function</badge> {#is-identifier-attribute-class-function}\n\nReturns whether the specified value is an `IdentifierAttribute` class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isIdentifierAttributeInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-identifier-attribute-instance-function}\n\nReturns whether the specified value is an `IdentifierAttribute` instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/identity-map-7sLdA5rsLvflf8T8OsZfGO.immutable.md",
    "content": "### IdentityMap <badge type=\"primary\">class</badge> {#identity-map-class}\n\nA class to manage the instances of the [`Component`](https://layrjs.com/docs/v2/reference/component) classes that are identifiable.\n\nA component class is identifiable when its prototype has a [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute).\n\nWhen a component class is identifiable, the `IdentityMap` ensures that there can only be one component instance with a specific identifier. So if you try to create two components with the same identifier, you will get an error.\n\n#### Usage\n\nYou shouldn't have to create an `IdentityMap` by yourself. Identity maps are created automatically for each [`Component`](https://layrjs.com/docs/v2/reference/component) class that are identifiable.\n\n**Example:**\n\nHere is a `Movie` component with an `id` primary identifier attribute:\n\n```\n// JS\n\nimport {Component, primaryIdentifier, attribute} from '@layr/component';\n\nclass Movie extends Component {\n  @primaryIdentifier() id;\n  @attribute('string') title;\n}\n```\n\n```\n// TS\n\nimport {Component, primaryIdentifier, attribute} from '@layr/component';\n\nclass Movie extends Component {\n  @primaryIdentifier() id!: string;\n  @attribute('string') title!: string;\n}\n```\n\nTo get the `IdentityMap` of the `Movie` component, simply do:\n\n```\nconst identityMap = Movie.getIdentityMap();\n```\n\nCurrently, the `IdentifyMap` provides only one public method — [`getComponent()`](https://layrjs.com/docs/v2/reference/identity-map#get-component-instance-method) — that allows to retrieve a component instance from its identifier:\n\n```\nconst movie = new Movie({id: 'abc123', title: 'Inception'});\n\nidentityMap.getComponent('abc123'); // => movie\n```\n\n#### Methods\n\n##### `getComponent(identifiers)` <badge type=\"secondary-outline\">instance method</badge> {#get-component-instance-method}\n\nGets a component instance from one of its identifiers. If there are no components corresponding to the specified identifiers, returns `undefined`.\n\n**Parameters:**\n\n* `identifiers`: A plain object specifying some identifiers. The shape of the object should be `{[identifierName]: identifierValue}`. Alternatively, you can specify a string or a number representing the value of the [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute) of the component you want to get.\n\n**Returns:**\n\nA [`Component`](https://layrjs.com/docs/v2/reference/component) instance or `undefined`.\n\n**Example:**\n\n```\n// JS\n\nimport {Component, primaryIdentifier, secondaryIdentifier} from '@layr/component';\n\nclass Movie extends Component {\n  @primaryIdentifier() id;\n  @secondaryIdentifier() slug;\n}\n\nconst movie = new Movie({id: 'abc123', slug: 'inception'});\n\nMovie.getIdentityMap().getComponent('abc123'); // => movie\nMovie.getIdentityMap().getComponent({id: 'abc123'}); // => movie\nMovie.getIdentityMap().getComponent({slug: 'inception'}); // => movie\nMovie.getIdentityMap().getComponent('xyx456'); // => undefined\n```\n```\n// TS\n\nimport {Component, primaryIdentifier, secondaryIdentifier} from '@layr/component';\n\nclass Movie extends Component {\n  @primaryIdentifier() id!: string;\n  @secondaryIdentifier() slug!: string;\n}\n\nconst movie = new Movie({id: 'abc123', slug: 'inception'});\n\nMovie.getIdentityMap().getComponent('abc123'); // => movie\nMovie.getIdentityMap().getComponent({id: 'abc123'}); // => movie\nMovie.getIdentityMap().getComponent({slug: 'inception'}); // => movie\nMovie.getIdentityMap().getComponent('xyx456'); // => undefined\n```\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/index-1Uz5JX1XB7V67nfM6tvnSV.immutable.md",
    "content": "### Index <badge type=\"primary\">class</badge> {#index-class}\n\nRepresents an index for one or several [attributes](https://layrjs.com/docs/v2/reference/attribute) of a [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class).\n\nOnce an index is defined for an attribute, all queries involving this attribute (through the [`find()`](https://layrjs.com/docs/v2/reference/storable#find-class-method) or the [`count()`](https://layrjs.com/docs/v2/reference/storable#count-class-method) methods) can be greatly optimized by the storable component's [store](https://layrjs.com/docs/v2/reference/store) and its underlying database.\n\n#### Usage\n\n##### Single Attribute Indexes\n\nTypically, you create an `Index` for a storable component's attribute by using the [`@index()`](https://layrjs.com/docs/v2/reference/storable#index-decorator) decorator. Then, you call the [`migrateStorables()`](https://layrjs.com/docs/v2/reference/store#migrate-storables-instance-method) method on the storable component's store to effectively create the index in the underlying database.\n\nFor example, here is how you would define a `Movie` class with some indexes:\n\n```js\n// JS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute, index} from '@layr/storable';\nimport {MongoDBStore} from '@layr/mongodb-store';\n\nexport class Movie extends Storable(Component) {\n  // Primary and secondary identifier attributes are automatically indexed,\n  // so there is no need to define an index for these types of attributes\n  @primaryIdentifier() id;\n\n  // Let's define an index for the `title` attribute\n  @index() @attribute('string') title;\n}\n\nconst store = new MongoDBStore('mongodb://user:pass@host:port/db');\n\nstore.registerStorable(Movie);\n```\n\n```ts\n// TS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute, index} from '@layr/storable';\nimport {MongoDBStore} from '@layr/mongodb-store';\n\nexport class Movie extends Storable(Component) {\n  // Primary and secondary identifier attributes are automatically indexed,\n  // so there is no need to define an index for these types of attributes\n  @primaryIdentifier() id!: string;\n\n  // Let's define an index for the `title` attribute\n  @index() @attribute('string') title!: string;\n}\n\nconst store = new MongoDBStore('mongodb://user:pass@host:port/db');\n\nstore.registerStorable(Movie);\n```\n\nThen you can call the [`migrateStorables()`](https://layrjs.com/docs/v2/reference/store#migrate-storables-instance-method) method on the store to create the indexes in the MongoDB database:\n\n```\nawait store.migrateStorables();\n```\n\nAnd now that the `title` attribute is indexed, you can make any query on this attribute in a very performant way:\n\n```\nconst movies = await Movie.find({title: 'Inception'});\n```\n\n##### Compound Attribute Indexes\n\nYou can create a compound attribute index to optimize some queries that involve a combination of attributes. To do so, you use the [`@index()`](https://layrjs.com/docs/v2/reference/storable#index-decorator) decorator on the storable component itself:\n\n```js\n// JS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute, index} from '@layr/storable';\nimport {MongoDBStore} from '@layr/mongodb-store';\n\n// Let's define a compound attribute index for the combination of the `year`\n// attribute (descending order) and the `title` attribute (ascending order)\n@index({year: 'desc', title: 'asc'})\nexport class Movie extends Storable(Component) {\n  @primaryIdentifier() id;\n\n  @attribute('string') title;\n\n  @attribute('number') year;\n}\n\nconst store = new MongoDBStore('mongodb://user:pass@host:port/db');\n\nstore.registerStorable(Movie);\n```\n\n```ts\n// TS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute, index} from '@layr/storable';\nimport {MongoDBStore} from '@layr/mongodb-store';\n\n// Let's define a compound attribute index for the combination of the `year`\n// attribute (descending order) and the `title` attribute (ascending order)\n@index({year: 'desc', title: 'asc'})\nexport class Movie extends Storable(Component) {\n  @primaryIdentifier() id!: string;\n\n  @attribute('string') title!: string;\n\n  @attribute('number') year!: number;\n}\n\nconst store = new MongoDBStore('mongodb://user:pass@host:port/db');\n\nstore.registerStorable(Movie);\n```\n\nThen you can call the [`migrateStorables()`](https://layrjs.com/docs/v2/reference/store#migrate-storables-instance-method) method on the store to create the compound attribute index in the MongoDB database:\n\n```\nawait store.migrateStorables();\n```\n\nAnd now you can make any query involving a combination of `year` and `title` in a very performant way:\n\n```\nconst movies = await Movie.find(\n  {year: {$greaterThan: 2010}},\n  true,\n  {sort: {year: 'desc', title: 'asc'}}\n);\n```\n\n#### Creation\n\n##### `new Index(attributes, parent, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates an instance of [`Index`](https://layrjs.com/docs/v2/reference/index).\n\n**Parameters:**\n\n* `attributes`: An object specifying the attributes to be indexed. The shape of the object should be `{attributeName: direction, ...}` where `attributeName` is a string representing the name of an attribute and `direction` is a string representing the sort direction (possible values: `'asc'` or `'desc'`).\n* `parent`: The storable component prototype that owns the index.\n* `options`:\n  * `isUnique`: A boolean specifying whether the index should hold unique values or not (default: `false`). When set to `true`, the underlying database will prevent you to store an attribute with the same value in multiple storable components.\n\n**Returns:**\n\nThe [`Index`](https://layrjs.com/docs/v2/reference/index) instance that was created.\n\n#### Basic Methods\n\n##### `getAttributes()` <badge type=\"secondary-outline\">instance method</badge> {#get-attributes-instance-method}\n\nReturns the indexed attributes.\n\n**Returns:**\n\nAn object of the shape `{attributeName: direction, ...}`.\n\n##### `getParent()` <badge type=\"secondary-outline\">instance method</badge> {#get-parent-instance-method}\n\nReturns the parent of the index.\n\n**Returns:**\n\nA storable component prototype.\n\n#### Utilities\n\n##### `isIndexClass(value)` <badge type=\"tertiary-outline\">function</badge> {#is-index-class-function}\n\nReturns whether the specified value is an `Index` class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isIndexInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-index-instance-function}\n\nReturns whether the specified value is an `Index` instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/memory-navigator-5wC9TEZn2cen7LuV4cGPOj.immutable.md",
    "content": "### MemoryNavigator <badge type=\"primary\">class</badge> {#memory-navigator-class}\n\n*Inherits from [`Navigator`](https://layrjs.com/docs/v2/reference/navigator).*\n\nA [`Navigator`](https://layrjs.com/docs/v2/reference/navigator) that keeps the navigation history in memory. Useful in tests and non-browser environments like [React Native](https://reactnative.dev/).\n\n#### Usage\n\nCreate a `MemoryNavigator` instance and register some [routable components](https://layrjs.com/docs/v2/reference/routable#routable-component-class) into it.\n\n#### Creation\n\n##### `new MemoryNavigator([options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates a [`MemoryNavigator`](https://layrjs.com/docs/v2/reference/memory-navigator).\n\n**Parameters:**\n\n* `options`:\n  * `initialURLs`: An array of URLs to populate the initial navigation history (default: `[]`).\n  * `initialIndex`: A number specifying the current entry's index in the navigation history (default: the index of the last entry in the navigation history).\n\n**Returns:**\n\nThe [`MemoryNavigator`](https://layrjs.com/docs/v2/reference/memory-navigator) instance that was created.\n\n#### Current Location\n\nSee the methods that are inherited from the [`Navigator`](https://layrjs.com/docs/v2/reference/navigator#current-location) class.\n\n#### Navigation\n\nSee the methods that are inherited from the [`Navigator`](https://layrjs.com/docs/v2/reference/navigator#navigation) class.\n\n#### Observability\n\nSee the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) class.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/memory-store-3QHbAhGolblHx4OY17rEP.immutable.md",
    "content": "### MemoryStore <badge type=\"primary\">class</badge> {#memory-store-class}\n\n*Inherits from [`Store`](https://layrjs.com/docs/v2/reference/store).*\n\nA [`Store`](https://layrjs.com/docs/v2/reference/store) that uses the memory to \"persist\" its registered [storable components](https://layrjs.com/docs/v2/reference/storable#storable-component-class). Since the stored data is wiped off every time the execution environment is restarted, a `MemoryStore` shouldn't be used for a real app.\n\n#### Usage\n\nCreate a `MemoryStore` instance, register some [storable components](https://layrjs.com/docs/v2/reference/storable#storable-component-class) into it, and then use any [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storable-component-class)'s method to load, save, delete, or find components from the store.\n\nSee an example of use in the [`MongoDBStore`](https://layrjs.com/docs/v2/reference/mongodb-store) class.\n\n#### Creation\n\n##### `new MemoryStore([options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates a [`MemoryStore`](https://layrjs.com/docs/v2/reference/memory-store).\n\n**Parameters:**\n\n* `options`:\n  * `initialCollections`: A plain object specifying the initial data that should be populated into the store. The shape of the objet should be `{[collectionName]: documents}` where `collectionName` is the name of a [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class) class, and `documents` is an array of serialized storable component instances.\n\n**Returns:**\n\nThe [`MemoryStore`](https://layrjs.com/docs/v2/reference/memory-store) instance that was created.\n\n**Example:**\n\n```\n// Create an empty memory store\nconst store = new MemoryStore();\n\n// Create a memory store with some initial data\nconst store = new MemoryStore({\n  User: [\n    {\n      __component: 'User',\n      id: 'xyz789',\n      email: 'user@domain.com'\n    }\n  ],\n  Movie: [\n    {\n      __component: 'Movie',\n      id: 'abc123',\n      title: 'Inception'\n    }\n  ]\n});\n```\n\n#### Component Registration\n\nSee the methods that are inherited from the [`Store`](https://layrjs.com/docs/v2/reference/store#component-registration) class.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/method-MAFTDipVXsOXj4o2CJLNQ.immutable.md",
    "content": "### Method <badge type=\"primary\">class</badge> {#method-class}\n\n*Inherits from [`Property`](https://layrjs.com/docs/v2/reference/property).*\n\nA `Method` represents a method of a [Component](https://layrjs.com/docs/v2/reference/component) class, prototype, or instance. It plays the role of a regular JavaScript method, but brings some extra features such as remote invocation, scheduled execution, or queuing.\n\n#### Usage\n\nTypically, you define a `Method` using the [`@method()`](https://layrjs.com/docs/v2/reference/component#method-decorator) decorator.\n\nFor example, here is how you would define a `Movie` class with some methods:\n\n```\nimport {Component, method} from '@layr/component';\n\nclass Movie extends Component {\n  // Class method\n  @method() static getConfig() {\n    // ...\n  }\n\n  // Instance method\n  @method() play() {\n    // ...\n  }\n}\n```\n\nThen you can call a method like you would normally do with regular JavaScript:\n\n```\nMovie.getConfig();\n\nconst movie = new Movie({title: 'Inception'});\nmovie.play();\n```\n\nSo far, you may wonder what is the point of defining methods this way. By itself the [`@method()`](https://layrjs.com/docs/v2/reference/component#method-decorator) decorator, except for creating a `Method` instance under the hood, doesn't provide much benefit.\n\nThe trick is that since you have a `Method`, you also have a [`Property`](https://layrjs.com/docs/v2/reference/property) (because `Method` inherits from `Property`), and properties can be exposed to remote access thanks to the [`@expose()`](https://layrjs.com/docs/v2/reference/component#expose-decorator) decorator.\n\nSo here is how you would expose the `Movie` methods:\n\n```\nimport {Component, method} from '@layr/component';\n\nclass Movie extends Component {\n  // Exposed class method\n  @expose({call: true}) @method() static getConfig() {\n    // ...\n  }\n\n  // Exposed instance method\n  @expose({call: true}) @method() play() {\n    // ...\n  }\n}\n```\n\nNow that you have some exposed methods, you can call them remotely in the same way you would do locally:\n\n```\nMovie.getConfig(); // Executed remotely\n\nconst movie = new Movie({title: 'Inception'});\nmovie.play();  // Executed remotely\n```\n\nIn addition, you can easily take advantage of some powerful features offered by [`Methods`](https://layrjs.com/docs/v2/reference/method). For example, here is how you would define a method that will be automatically executed every hour:\n\n```\nclass Application extends Component {\n  @method({schedule: {rate: 60 * 60 * 1000}}) static async runHourlyTask() {\n    // Do something every hour...\n  }\n}\n```\n\nAnd here is how you would define a method that will be executed in background with a maximum duration of 5 minutes:\n\n```\nclass Email extends Component {\n  @method({queue: true, maximumDuration: 5 * 60 * 1000}) async send() {\n    // Do something in background\n  }\n}\n```\n\n#### Creation\n\n##### `new Method(name, parent, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates an instance of [`Method`](https://layrjs.com/docs/v2/reference/method). Typically, instead of using this constructor, you would rather use the [`@method()`](https://layrjs.com/docs/v2/reference/component#method-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the method.\n* `parent`: The component class, prototype, or instance that owns the method.\n* `options`:\n  * `exposure`: A [`PropertyExposure`](https://layrjs.com/docs/v2/reference/property#property-exposure-type) object specifying how the method should be exposed to remote calls.\n  * `schedule`: A [`MethodScheduling`](https://layrjs.com/docs/v2/reference/method#method-scheduling-type) object specifying how the method should be scheduled for automatic execution. Note that only static methods can be scheduled.\n  * `queue`: A boolean specifying whether the method should be executed in background.\n  * `maximumDuration`: A number specifying the maximum duration of the method in milliseconds. Note that the actual duration of the method execution is not currently enforced. The purpose of this option is to help configuring the deployment of serverless functions. For example, in case of deployment to [AWS Lambda](https://aws.amazon.com/lambda/), this option will affect the `timeout` property of the generated Lambda function.\n\n**Returns:**\n\nThe [`Method`](https://layrjs.com/docs/v2/reference/method) instance that was created.\n\n**Example:**\n\n```\nimport {Component, Method} from '@layr/component';\n\nclass Movie extends Component {}\n\nconst play = new Method('play', Movie.prototype, {exposure: {call: true}});\n\nplay.getName(); // => 'play'\nplay.getParent(); // => Movie.prototype\nplay.getExposure(); // => {call: true}\n```\n\n#### Methods\n\nSee the methods that are inherited from the [`Property`](https://layrjs.com/docs/v2/reference/property#basic-methods) class.\n\n#### Scheduling\n\n##### `getScheduling()` <badge type=\"secondary-outline\">instance method</badge> {#get-scheduling-instance-method}\n\nIf the method is scheduled for automatic execution, returns a [`MethodScheduling`](https://layrjs.com/docs/v2/reference/method#method-scheduling-type) object. Otherwise, returns `undefined`.\n\n**Returns:**\n\nA [`MethodScheduling`](https://layrjs.com/docs/v2/reference/method#method-scheduling-type) object or `undefined`.\n\n**Example:**\n\n```\nrunHourlyTaskMethod.getScheduling(); // => {rate: 60 * 60 * 1000}\nregularMethod.getScheduling(); // => undefined\n```\n\n##### `setScheduling(scheduling)` <badge type=\"secondary-outline\">instance method</badge> {#set-scheduling-instance-method}\n\nSets how the method should be scheduled for automatic execution. Note that only static methods can be scheduled.\n\n**Parameters:**\n\n* `scheduling`: A [`MethodScheduling`](https://layrjs.com/docs/v2/reference/method#method-scheduling-type) object.\n\n**Example:**\n\n```\nrunHourlyTaskMethod.setScheduling({rate: 60 * 60 * 1000});\n```\n\n##### `MethodScheduling` <badge type=\"primary-outline\">type</badge> {#method-scheduling-type}\n\nA `MethodScheduling` is a plain object specifying how a method is scheduled for automatic execution. The shape of the object is `{rate: number}` where `rate` is the execution frequency expressed in milliseconds.\n\n**Example:**\n\n```\n{rate: 60 * 1000} // Every minute\n{rate: 60 * 60 * 1000} // Every hour\n{rate: 24 * 60 * 60 * 1000} // Every day\n```\n\n#### Queueing\n\n##### `getQueueing()` <badge type=\"secondary-outline\">instance method</badge> {#get-queueing-instance-method}\n\nReturns `true` if the method should be executed in background. Otherwise, returns `undefined`.\n\n**Returns:**\n\nA boolean or `undefined`.\n\n**Example:**\n\n```\nbackgroundMethod.getQueueing(); // => true\nregularMethod.getQueueing(); // => undefined\n```\n\n##### `setQueueing(queueing)` <badge type=\"secondary-outline\">instance method</badge> {#set-queueing-instance-method}\n\nSets whether the method should be executed in background.\n\n**Parameters:**\n\n* `queueing`: Pass `true` to specify that the method should be executed in background. Otherwise, you can pass `false` or `undefined`.\n\n**Example:**\n\n```\nbackgroundMethod.setQueueing(true);\n```\n\n#### Maximum Duration\n\n##### `getMaximumDuration()` <badge type=\"secondary-outline\">instance method</badge> {#get-maximum-duration-instance-method}\n\nReturns a number representing the maximum duration of the method in milliseconds or `undefined` if the method has no maximum duration.\n\n**Returns:**\n\nA number or `undefined`.\n\n**Example:**\n\n```\nbackgroundMethod.getMaximumDuration(); // => 5 * 60 * 1000 (5 minutes)\nregularMethod.getMaximumDuration(); // => undefined\n```\n\n##### `setMaximumDuration(maximumDuration)` <badge type=\"secondary-outline\">instance method</badge> {#set-maximum-duration-instance-method}\n\nSets the maximum duration of the method in milliseconds. Alternatively, you can pass `undefined` to indicate that the method has no maximum duration.\n\n**Parameters:**\n\n* `maximumDuration`: A number or `undefined`.\n\n**Example:**\n\n```\nbackgroundMethod.setMaximumDuration(5 * 60 * 1000); // 5 minutes\n```\n\n#### Utilities\n\n##### `isMethodClass(value)` <badge type=\"tertiary-outline\">function</badge> {#is-method-class-function}\n\nReturns whether the specified value is a `Method` class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isMethodInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-method-instance-function}\n\nReturns whether the specified value is a `Method` instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/mongodb-store-37qsYI8A5ctkFlHTDVIc3O.immutable.md",
    "content": "### MongoDBStore <badge type=\"primary\">class</badge> {#mongo-db-store-class}\n\n*Inherits from [`Store`](https://layrjs.com/docs/v2/reference/store).*\n\nA [`Store`](https://layrjs.com/docs/v2/reference/store) that uses a [MongoDB](https://www.mongodb.com/) database to persist its registered [storable components](https://layrjs.com/docs/v2/reference/storable#storable-component-class).\n\n#### Usage\n\nCreate a `MongoDBStore` instance, register some [storable components](https://layrjs.com/docs/v2/reference/storable#storable-component-class) into it, and then use any [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storable-component-class)'s method to load, save, delete, or find components from the store.\n\nFor example, let's build a simple `Backend` that provides a `Movie` component.\n\nFirst, let's define the components that we are going to use:\n\n```\n// JS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  @primaryIdentifier() id;\n\n  @attribute() title = '';\n}\n\nclass Backend extends Component {\n  @provide() static Movie = Movie;\n}\n```\n\n```\n// TS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  @primaryIdentifier() id!: string;\n\n  @attribute() title = '';\n}\n\nclass Backend extends Component {\n  @provide() static Movie = Movie;\n}\n```\n\nNext, let's create a `MongoDBStore` instance, and let's register the `Backend` component as the root component of the store:\n\n```\nimport {MongoDBStore} from '@layr/mongodb-store';\n\nconst store = new MongoDBStore('mongodb://user:pass@host:port/db');\n\nstore.registerRootComponent(Backend);\n```\n\nFinally, we can interact with the store by calling some [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storable-component-class) methods:\n\n```\nlet movie = new Movie({id: 'abc123', title: 'Inception'});\n\n// Save the movie to the store\nawait movie.save();\n\n// Get the movie from the store\nmovie = await Movie.get('abc123');\nmovie.title; // => 'Inception'\n\n// Modify the movie, and save it to the store\nmovie.title = 'Inception 2';\nawait movie.save();\n\n// Find the movies that have a title starting with 'Inception'\nconst movies = await Movie.find({title: {$startsWith: 'Inception'}});\nmovies.length; // => 1 (one movie found)\nmovies[0].title; // => 'Inception 2'\nmovies[0] === movie; // => true (thanks to the identity mapping)\n\n// Delete the movie from the store\nawait movie.delete();\n```\n\n#### Creation\n\n##### `new MongoDBStore(connectionString, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates a [`MongoDBStore`](https://layrjs.com/docs/v2/reference/mongodb-store).\n\n**Parameters:**\n\n* `connectionString`: The [connection string](https://docs.mongodb.com/manual/reference/connection-string/) of the MongoDB database to use.\n* `options`:\n  * `poolSize`: A number specifying the maximum size of the connection pool (default: `1`).\n\n**Returns:**\n\nThe [`MongoDBStore`](https://layrjs.com/docs/v2/reference/mongodb-store) instance that was created.\n\n**Example:**\n\n```\nconst store = new MongoDBStore('mongodb://user:pass@host:port/db');\n```\n\n#### Component Registration\n\nSee the methods that are inherited from the [`Store`](https://layrjs.com/docs/v2/reference/store#component-registration) class.\n\n#### Connection to MongoDB\n\n##### `connect()` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#connect-instance-method}\n\nInitiates a connection to the MongoDB database.\n\nSince this method is called automatically when you interact with the store through any of the [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storable-component-class) methods, you shouldn't have to call it manually.\n\n##### `disconnect()` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#disconnect-instance-method}\n\nCloses the connection to the MongoDB database. Unless you are building a tool that uses a store for an ephemeral duration, you shouldn't have to call this method.\n\n#### Migration\n\nSee the methods that are inherited from the [`Store`](https://layrjs.com/docs/v2/reference/store#migration) class.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/navigator-5vNC9hp62PFUvxh0PLQcJ9.immutable.md",
    "content": "### Navigator <badge type=\"primary\">class</badge> {#navigator-class}\n\n*Inherits from [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class).*\n\nAn abstract class from which classes such as [`BrowserNavigator`](https://layrjs.com/docs/v2/reference/browser-navigator) or [`MemoryNavigator`](https://layrjs.com/docs/v2/reference/memory-navigator) are constructed. Unless you build a custom navigator, you probably won't have to use this class directly.\n\n#### Current Location\n\n##### `getCurrentURL()` <badge type=\"secondary-outline\">instance method</badge> {#get-current-url-instance-method}\n\nReturns the current URL of the navigator.\n\n**Returns:**\n\nA string.\n\n**Example:**\n\n```\n// See the definition of `navigator` in the `findRouteByURL()` example\n\nnavigator.navigate('/movies/inception?showDetails=1#actors');\nnavigator.getCurrentURL(); // => /movies/inception?showDetails=1#actors'\n```\n\n##### `getCurrentPath()` <badge type=\"secondary-outline\">instance method</badge> {#get-current-path-instance-method}\n\nReturns the path of the current URL.\n\n**Returns:**\n\nA string.\n\n**Example:**\n\n```\n// See the definition of `navigator` in the `findRouteByURL()` example\n\nnavigator.navigate('/movies/inception?showDetails=1#actors');\nnavigator.getCurrentPath(); // => '/movies/inception'\n```\n\n##### `getCurrentQuery()` <badge type=\"secondary-outline\">instance method</badge> {#get-current-query-instance-method}\n\nReturns an object representing the query of the current URL.\n\nThe [`qs`](https://github.com/ljharb/qs) package is used under the hood to parse the query.\n\n**Returns:**\n\nA plain object.\n\n**Example:**\n\n```\n// See the definition of `navigator` in the `findRouteByURL()` example\n\nnavigator.navigate('/movies/inception?showDetails=1#actors');\nnavigator.getCurrentQuery(); // => {showDetails: '1'}\n```\n\n##### `getCurrentHash()` <badge type=\"secondary-outline\">instance method</badge> {#get-current-hash-instance-method}\n\nReturns the hash (i.e., the [fragment identifier](https://en.wikipedia.org/wiki/URI_fragment)) contained in the current URL. If the current URL doesn't contain a hash, returns `undefined`.\n\n**Returns:**\n\nA string or `undefined`.\n\n**Example:**\n\n```\n// See the definition of `navigator` in the `findRouteByURL()` example\n\nnavigator.navigate('/movies/inception?showDetails=1#actors');\nnavigator.getCurrentHash(); // => 'actors'\n\nnavigator.navigate('/movies/inception?showDetails=1#actors');\nnavigator.getCurrentHash(); // => 'actors'\n\nnavigator.navigate('/movies/inception?showDetails=1#');\nnavigator.getCurrentHash(); // => undefined\n\nnavigator.navigate('/movies/inception?showDetails=1');\nnavigator.getCurrentHash(); // => undefined\n```\n\n#### Navigation\n\n##### `navigate(url, [options])` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">possibly async</badge> {#navigate-instance-method}\n\nNavigates to a URL.\n\nThe specified URL is added to the navigator's history.\n\nThe observers of the navigator are automatically called.\n\nNote that instead of using this method, you can use the handy `navigate()` shortcut function that you get when you define a route with the [`@route()`](https://layrjs.com/docs/v2/reference/routable#route-decorator) decorator.\n\n**Parameters:**\n\n* `url`: A string or a [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) object.\n* `options`:\n  * `silent`: A boolean specifying whether the navigator's observers should *not* be called (default: `false`).\n  * `defer`: A boolean specifying whether the calling of the navigator's observers should be deferred to the next tick (default: `true`).\n\n**Example:**\n\n```\nnavigator.navigate('/movies/inception');\n\n// Same as above, but in a more idiomatic way:\nMovie.Viewer.navigate({slug: 'inception});\n```\n\n##### `redirect(url, [options])` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">possibly async</badge> {#redirect-instance-method}\n\nRedirects to a URL.\n\nThe specified URL replaces the current entry of the navigator's history.\n\nThe observers of the navigator are automatically called.\n\nNote that instead of using this method, you can use the handy `redirect()` shortcut function that you get when you define a route with the [`@route()`](https://layrjs.com/docs/v2/reference/routable#route-decorator) decorator.\n\n**Parameters:**\n\n* `url`: A string or a [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) object.\n* `options`:\n  * `silent`: A boolean specifying whether the navigator's observers should *not* be called (default: `false`).\n  * `defer`: A boolean specifying whether the calling of the navigator's observers should be deferred to the next tick (default: `true`).\n\n**Example:**\n\n```\nnavigator.redirect('/sign-in');\n\n// Same as above, but in a more idiomatic way:\nSession.SignIn.redirect();\n```\n\n##### `reload(url)` <badge type=\"secondary-outline\">instance method</badge> {#reload-instance-method}\n\nReloads the execution environment with the specified URL.\n\nNote that instead of using this method, you can use the handy `reload()` shortcut function that you get when you define a route with the [`@route()`](https://layrjs.com/docs/v2/reference/routable#route-decorator) decorator.\n\n**Parameters:**\n\n* `url`: A string or a [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) object.\n\n**Example:**\n\n```\nnavigator.reload('/');\n\n// Same as above, but in a more idiomatic way:\nFrontend.Home.reload();\n```\n\n##### `go(delta, [options])` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">possibly async</badge> {#go-instance-method}\n\nMove forwards or backwards through the navigator's history.\n\nThe observers of the navigator are automatically called.\n\n**Parameters:**\n\n* `delta`: A number representing the position in the navigator's history to which you want to move, relative to the current entry. A negative value moves backwards, a positive value moves forwards.\n* `options`:\n  * `silent`: A boolean specifying whether the navigator's observers should *not* be called (default: `false`).\n  * `defer`: A boolean specifying whether the calling of the navigator's observers should be deferred to the next tick (default: `true`).\n\n**Example:**\n\n```\nnavigator.go(-2); // Move backwards by two entries of the navigator's history\n\nnavigator.go(-1); // Equivalent of calling `navigator.goBack()`\n\nnavigator.go(1); // Equivalent of calling `navigator.goForward()`\n\nnavigator.go(2); // Move forward two entries of the navigator's history\n```\n\n##### `goBack([options])` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">possibly async</badge> {#go-back-instance-method}\n\nGo back to the previous entry in the navigator's history.\n\nThis method is the equivalent of calling `navigator.go(-1)`.\n\nThe observers of the navigator are automatically called.\n\n**Parameters:**\n\n* `options`:\n  * `silent`: A boolean specifying whether the navigator's observers should *not* be called (default: `false`).\n  * `defer`: A boolean specifying whether the calling of the navigator's observers should be deferred to the next tick (default: `true`).\n\n##### `goBackToRoot([options])` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">possibly async</badge> {#go-back-to-root-instance-method}\n\nGo back to the first entry in the navigator's history.\n\nThe observers of the navigator are automatically called.\n\n**Parameters:**\n\n* `options`:\n  * `silent`: A boolean specifying whether the navigator's observers should *not* be called (default: `false`).\n  * `defer`: A boolean specifying whether the calling of the navigator's observers should be deferred to the next tick (default: `true`).\n\n##### `goForward([options])` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">possibly async</badge> {#go-forward-instance-method}\n\nGo forward to the next entry in the navigator's history.\n\nThis method is the equivalent of calling `navigator.go(1)`.\n\nThe observers of the navigator are automatically called.\n\n**Parameters:**\n\n* `options`:\n  * `silent`: A boolean specifying whether the navigator's observers should *not* be called (default: `false`).\n  * `defer`: A boolean specifying whether the calling of the navigator's observers should be deferred to the next tick (default: `true`).\n\n##### `getHistoryLength()` <badge type=\"secondary-outline\">instance method</badge> {#get-history-length-instance-method}\n\nReturns the number of entries in the navigator's history.\n\n##### `getHistoryIndex()` <badge type=\"secondary-outline\">instance method</badge> {#get-history-index-instance-method}\n\nReturns the current index in the navigator's history.\n\n#### Observability\n\nSee the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) class.\n\n#### Utilities\n\n##### `isNavigatorClass(value)` <badge type=\"tertiary-outline\">function</badge> {#is-navigator-class-function}\n\nReturns whether the specified value is a navigator class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isNavigatorInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-navigator-instance-function}\n\nReturns whether the specified value is a navigator instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/observable-7alzx5bqV1gD2Aj60hWQ0L.immutable.md",
    "content": "### Observable() <badge type=\"primary\">mixin</badge> {#observable-mixin}\n\nBrings observability to any class.\n\nThis mixin is used to construct several Layr's classes such as [`Component`](https://layrjs.com/docs/v2/reference/component) or [`Attribute`](https://layrjs.com/docs/v2/reference/attribute). So, in most cases, you'll have the capabilities provided by this mixin without having to call it.\n\n#### Usage\n\nCall the `Observable()` mixin with any class to construct an [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) class. Then, you can add some observers by using the [`addObserver()`](https://layrjs.com/docs/v2/reference/observable#add-observer-dual-method) method, and trigger their execution anytime by using the [`callObservers()`](https://layrjs.com/docs/v2/reference/observable#call-observers-dual-method) method.\n\nFor example, let's define a `Movie` class using the `Observable()` mixin:\n\n```\n// JS\n\nimport {Observable} from '@layr/observable';\n\nclass Movie extends Observable(Object) {\n  get title() {\n    return this._title;\n  }\n\n  set title(title) {\n    this._title = title;\n    this.callObservers();\n  }\n}\n```\n\n```\n// TS\n\nimport {Observable} from '@layr/observable';\n\nclass Movie extends Observable(Object) {\n  _title?: string;\n\n  get title() {\n    return this._title;\n  }\n\n  set title(title: string) {\n    this._title = title;\n    this.callObservers();\n  }\n}\n```\n\nNext, we can create a `Movie` instance, and observe it:\n\n```\nconst movie = new Movie();\n\nmovie.addObserver(() => {\n  console.log('The movie's title has changed');\n})\n```\n\nAnd now, every time we change the title of `movie`, its observer will be automatically executed:\n\n```\nmovie.title = 'Inception';\n\n// Should display:\n// 'The movie's title has changed'\n```\n\n> Note that the same result could have been achieved by using a Layr [`Component`](https://layrjs.com/docs/v2/reference/component):\n>\n> ```\n> // JS\n>\n> import {Component, attribute} from '@layr/component';\n>\n> class Movie extends Component {\n>   @attribute('string?') title;\n> }\n> ```\n>\n> ```\n> // TS\n>\n> import {Component, attribute} from '@layr/component';\n>\n> class Movie extends Component {\n>   @attribute('string?') title?: string;\n> }\n> ```\n\n### Observable <badge type=\"primary\">class</badge> {#observable-class}\n\nAn `Observable` class is constructed by calling the `Observable()` mixin ([see above](https://layrjs.com/docs/v2/reference/observable#observable-mixin)).\n\n#### Methods\n\n##### `addObserver(observer)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#add-observer-dual-method}\n\nAdds an observer to the current class or instance.\n\n**Parameters:**\n\n* `observer`: A function that will be automatically executed when the [`callObservers()`](https://layrjs.com/docs/v2/reference/observable#call-observers-dual-method) method is called. Alternatively, you can specify an observable for which the observers should be executed, and doing so, you can connect an observable to another observable.\n\n**Example:**\n\n```\nMovie.addObserver(() => {\n  // A `Movie` class observer\n});\n\nconst movie = new Movie();\n\nmovie.addObserver(() => {\n  // A `Movie` instance observer\n});\n\nconst actor = new Actor();\n\n// Connect `actor` to `movie` so that when `callObservers()` is called on `actor`,\n// then `callObservers()` is automatically called on `movie`\nactor.addObserver(movie);\n```\n\n##### `removeObserver(observer)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#remove-observer-dual-method}\n\nRemoves an observer from the current class or instance.\n\n**Parameters:**\n\n* `observer`: A function or a connected observable.\n\n**Example:**\n\n```\nconst observer = () => {\n  // ...\n}\n\n// Add `observer` to the `Movie` class\nMovie.addObserver(observer);\n\n// Remove `observer` from to the `Movie` class\nMovie.removeObserver(observer);\n\nconst movie = new Movie();\nconst actor = new Actor();\n\n// Connect `actor` to `movie`\nactor.addObserver(movie);\n\n// Remove the connection between `actor` and `movie`\nactor.removeObserver(movie);\n```\n\n##### `callObservers([payload])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#call-observers-dual-method}\n\nCalls the observers of the current class or instance.\n\n**Parameters:**\n\n* `payload`: An optional object to pass to the observers when they are executed.\n\n**Example:**\n\n```\nconst movie = new Movie();\n\nmovie.addObserver((payload) => {\n  console.log('Observer called with:', payload);\n});\n\nmovie.callObservers();\n\n// Should display:\n// 'Observer called with: undefined'\n\nmovie.callObservers({changes: ['title']});\n\n// Should display:\n// 'Observer called with: {changes: ['title']}'\n```\n\n#### Bringing Observability to an Object or an Array\n\n##### `createObservable(target)` <badge type=\"tertiary-outline\">function</badge> {#create-observable-function}\n\nReturns an observable from an existing object or array.\n\nThe returned observable is observed deeply. So, for example, if an object contains a nested object, modifying the nested object will trigger the execution of the parent's observers.\n\nThe returned observable provides the same methods as an [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) instance:\n\n- [`addObserver()`](https://layrjs.com/docs/v2/reference/observable#add-observer-dual-method)\n- [`removeObserver()`](https://layrjs.com/docs/v2/reference/observable#remove-observer-dual-method)\n- [`callObservers()`](https://layrjs.com/docs/v2/reference/observable#call-observers-dual-method)\n\n**Parameters:**\n\n* `target`: A JavaScript plain object or array that you want to observe.\n\n**Returns:**\n\nAn observable objet or array.\n\n**Example:**\n\n```\nimport {createObservable} from '@layr/observable';\n\n// Create an observable `movie`\nconst movie = createObservable({\n  title: 'Inception',\n  genres: ['drama'],\n  details: {duration: 120}\n});\n\n// Add an observer\nmovie.addObserver(() => {\n  // ...\n});\n\n// Then, any of the following changes on `movie` will call the observer:\nmovie.title = 'Inception 2';\ndelete movie.title;\nmovie.year = 2010;\nmovie.genres.push('action');\nmovie.genres[1] = 'sci-fi';\nmovie.details.duration = 125;\n```\n\n#### Utilities\n\n##### `isObservable(value)` <badge type=\"tertiary-outline\">function</badge> {#is-observable-function}\n\nReturns whether the specified value is observable. When a value is observable, you can use any the following methods on it: [`addObserver()`](https://layrjs.com/docs/v2/reference/observable#add-observer-dual-method), [`removeObserver()`](https://layrjs.com/docs/v2/reference/observable#remove-observer-dual-method), and [`callObservers()`](https://layrjs.com/docs/v2/reference/observable#call-observers-dual-method).\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/primary-identifier-attribute-5EqFynrkNUTa9DxYYmbmOl.immutable.md",
    "content": "### PrimaryIdentifierAttribute <badge type=\"primary\">class</badge> {#primary-identifier-attribute-class}\n\n*Inherits from [`IdentifierAttribute`](https://layrjs.com/docs/v2/reference/identifier-attribute).*\n\nA `PrimaryIdentifierAttribute` is a special kind of attribute that uniquely identify a [Component](https://layrjs.com/docs/v2/reference/component) instance.\n\nA `Component` can have only one `PrimaryIdentifierAttribute`. To define a `Component` with more than one identifier, you can add some [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute) in addition to the `PrimaryIdentifierAttribute`.\n\nAnother characteristic of a `PrimaryIdentifierAttribute` is that its value is immutable (i.e., once set it cannot change). This ensures a stable identity of the components across the different layers of an app (e.g., frontend, backend, and database).\n\nWhen a `Component` has a `PrimaryIdentifierAttribute`, its instances are managed by an [`IdentityMap`](https://layrjs.com/docs/v2/reference/identity-map) ensuring that there can only be one instance with a specific identifier.\n\n#### Usage\n\nTypically, you create a `PrimaryIdentifierAttribute` and associate it to a component prototype using the [`@primaryIdentifier()`](https://layrjs.com/docs/v2/reference/component#primary-identifier-decorator) decorator.\n\nFor example, here is how you would define a `Movie` class with an `id` primary identifier attribute:\n\n```\n// JS\n\nimport {Component, primaryIdentifier, attribute} from '@layr/component';\n\nclass Movie extends Component {\n  // An auto-generated 'string' primary identifier attribute\n  @primaryIdentifier() id;\n\n  // A regular attribute\n  @attribute('string') title;\n}\n```\n\n```\n// TS\n\nimport {Component, primaryIdentifier, attribute} from '@layr/component';\n\nclass Movie extends Component {\n  // An auto-generated 'string' primary identifier attribute\n  @primaryIdentifier() id!: string;\n\n  // A regular attribute\n  @attribute('string') title!: string;\n}\n```\n\nThen, to create a `Movie` instance, you would do something like:\n\n```\nconst movie = new Movie({title: 'Inception'});\n\nmovie.id; // => 'ck41vli1z00013h5xx1esffyn'\nmovie.title; // => 'Inception'\n```\n\nNote that we didn't have to specify a value for the `id` attribute; it was automatically generated (using the [`Component.generateId()`](https://layrjs.com/docs/v2/reference/component#generate-id-class-method) method under the hood).\n\nTo create a `Movie` instance with an `id` of your choice, just do:\n\n```\nconst movie = new Movie({id: 'abc123', title: 'Inception'});\n\nmovie.id; // => 'abc123'\nmovie.title; // => 'Inception'\n```\n\nAs mentioned previously, when a component has a primary identifier attribute, all its instances are managed by an [`IdentityMap`](https://layrjs.com/docs/v2/reference/identity-map) ensuring that there is only one instance with a specific identifier.\n\nSo, since we previously created a `Movie` with `'abc123'` as primary identifier, we cannot create another `Movie` with the same primary identifier:\n\n```\nnew Movie({id: 'abc123', title: 'Inception 2'}); // => Error\n```\n\n`PrimaryIdentifierAttribute` values are usually of type `'string'` (the default), but you can also have values of type `'number'`:\n\n```\n// JS\n\nimport {Component, primaryIdentifier} from '@layr/component';\n\nclass Movie extends Component {\n  // An auto-generated 'number' primary identifier attribute\n  @primaryIdentifier('number') id = Math.random();\n}\n```\n\n```\n// TS\n\nimport {Component, primaryIdentifier} from '@layr/component';\n\nclass Movie extends Component {\n  // An auto-generated 'number' primary identifier attribute\n  @primaryIdentifier('number') id = Math.random();\n}\n```\n\n#### Creation\n\n##### `new PrimaryIdentifierAttribute(name, parent, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates an instance of [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute). Typically, instead of using this constructor, you would rather use the [`@primaryIdentifier()`](https://layrjs.com/docs/v2/reference/component#primary-identifier-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the attribute.\n* `parent`: The component prototype that owns the attribute.\n* `options`:\n  * `valueType`: A string specifying the type of values the attribute can store. It can be either `'string'` or `'number'` (default: `'string'`).\n  * `default`: A function returning the default value of the attribute (default when `valueType` is `'string'`: `function () { return this.constructor.generateId() }`).\n  * `validators`: An array of [validators](https://layrjs.com/docs/v2/reference/validator) for the value of the attribute.\n  * `exposure`: A [`PropertyExposure`](https://layrjs.com/docs/v2/reference/property#property-exposure-type) object specifying how the attribute should be exposed to remote access.\n\n**Returns:**\n\nThe [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute) instance that was created.\n\n**Example:**\n\n```\nimport {Component, PrimaryIdentifierAttribute} from '@layr/component';\n\nclass Movie extends Component {}\n\nconst id = new PrimaryIdentifierAttribute('id', Movie.prototype);\n\nid.getName(); // => 'id'\nid.getParent(); // => Movie.prototype\nid.getValueType().toString(); // => 'string'\nid.getDefaultValue(); // => function () { return this.constructor.generateId() }`\n```\n\n#### Property Methods\n\nSee the methods that are inherited from the [`Property`](https://layrjs.com/docs/v2/reference/property#basic-methods) class.\n\n#### Attribute Methods\n\nSee the methods that are inherited from the [`Attribute`](https://layrjs.com/docs/v2/reference/attribute#value-type) class.\n\n#### Observability\n\nSee the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) class.\n\n#### Utilities\n\n##### `isPrimaryIdentifierAttributeClass(value)` <badge type=\"tertiary-outline\">function</badge> {#is-primary-identifier-attribute-class-function}\n\nReturns whether the specified value is a `PrimaryIdentifierAttribute` class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isPrimaryIdentifierAttributeInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-primary-identifier-attribute-instance-function}\n\nReturns whether the specified value is a `PrimaryIdentifierAttribute` instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/property-bs5Xm31fCQXQkoZL24prZ.immutable.md",
    "content": "### Property <badge type=\"primary\">class</badge> {#property-class}\n\nA base class from which classes such as [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) or [`Method`](https://layrjs.com/docs/v2/reference/method) are constructed. Unless you build a custom property class, you probably won't have to use this class directly.\n\n#### Creation\n\n##### `new Property(name, parent, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates an instance of [`Property`](https://layrjs.com/docs/v2/reference/property).\n\n**Parameters:**\n\n* `name`: The name of the property.\n* `parent`: The component class, prototype, or instance that owns the property.\n* `options`:\n  * `exposure`: A [`PropertyExposure`](https://layrjs.com/docs/v2/reference/property#property-exposure-type) object specifying how the property should be exposed to remote access.\n\n**Returns:**\n\nThe [`Property`](https://layrjs.com/docs/v2/reference/property) instance that was created.\n\n**Example:**\n\n```\nimport {Component, Property} from '@layr/component';\n\nclass Movie extends Component {}\n\nconst titleProperty = new Property('title', Movie.prototype);\n\ntitleProperty.getName(); // => 'title'\ntitleProperty.getParent(); // => Movie.prototype\n```\n\n#### Basic Methods\n\n##### `getName()` <badge type=\"secondary-outline\">instance method</badge> {#get-name-instance-method}\n\nReturns the name of the property.\n\n**Returns:**\n\nA string.\n\n**Example:**\n\n```\ntitleProperty.getName(); // => 'title'\n```\n\n##### `getParent()` <badge type=\"secondary-outline\">instance method</badge> {#get-parent-instance-method}\n\nReturns the parent of the property.\n\n**Returns:**\n\nA component class, prototype, or instance.\n\n**Example:**\n\n```\ntitleProperty.getParent(); // => Movie.prototype\n```\n\n#### Exposure\n\n##### `getExposure()` <badge type=\"secondary-outline\">instance method</badge> {#get-exposure-instance-method}\n\nReturns an object specifying how the property is exposed to remote access.\n\n**Returns:**\n\nA [`PropertyExposure`](https://layrjs.com/docs/v2/reference/property#property-exposure-type) object.\n\n**Example:**\n\n```\ntitleProperty.getExposure(); // => {get: true, set: true}\n```\n\n##### `setExposure([exposure])` <badge type=\"secondary-outline\">instance method</badge> {#set-exposure-instance-method}\n\nSets how the property is exposed to remote access.\n\n**Parameters:**\n\n* `exposure`: A [`PropertyExposure`](https://layrjs.com/docs/v2/reference/property#property-exposure-type) object.\n\n**Example:**\n\n```\ntitleProperty.setExposure({get: true, set: true});\n```\n\n##### `operationIsAllowed(operation)` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">possibly async</badge> {#operation-is-allowed-instance-method}\n\nReturns whether an operation is allowed on the property.\n\n**Parameters:**\n\n* `operation`: A string representing an operation. Currently supported operations are 'get', 'set', and 'call'.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\ntitleProperty.operationIsAllowed('get'); // => true\ntitleProperty.operationIsAllowed('call'); // => false\n```\n\n##### `PropertyExposure` <badge type=\"primary-outline\">type</badge> {#property-exposure-type}\n\nA `PropertyExposure` is a plain object specifying how a property is exposed to remote access.\n\nThe shape of the object is `{[operation]: permission}` where:\n\n- `operation` is a string representing the different types of operations (`'get'` and `'set'` for attributes, and `'call'` for methods).\n- `permission` is a boolean (or a string or array of strings if the [`WithRoles`](https://layrjs.com/docs/v2/reference/with-roles) mixin is used) specifying whether the operation is allowed or not.\n\n**Example:**\n\n```\n{get: true, set: true}\n{get: 'anyone', set: ['author', 'admin']}\n{call: true}\n{call: 'admin'}\n```\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/query-146gEiaHLp06V3LqhxRCzC.immutable.md",
    "content": "### Query <badge type=\"primary-outline\">type</badge> {#query-type}\n\nA plain object specifying the criteria to be used when selecting some components from a store with the methods [`StorableComponent.find()`](https://layrjs.com/docs/v2/reference/storable#find-class-method) or [`StorableComponent.count()`](https://layrjs.com/docs/v2/reference/storable#count-class-method).\n\n#### Basic Queries\n\n##### Empty Query\n\nSpecify an empty object (`{}`) to select all the components:\n\n```\n// Find all the movies\nawait Movie.find({});\n```\n\n##### Single Attribute Query\n\nSpecify an object composed of an attribute's name and value to select the components that have an attribute's value equals to a specific value:\n\n```\n// Find the Japanese movies\nawait Movie.find({country: 'Japan'});\n\n// Find the unreleased movies\nawait Movie.find({year: undefined});\n```\n\n##### Multiple Attributes Query\n\nYou can combine several attributes' name and value to select the components by multiple attributes:\n\n```\n// Find the Japanese drama movies\nawait Movie.find({country: 'Japan', genre: 'drama'});\n```\n\n#### Basic Operators\n\nInstead of a specific value, you can specify an object containing one or more operators to check whether the value of an attribute matches certain criteria.\n\n##### `$equal`\n\nUse the `$equal` operator to check whether the value of an attribute is equal to a specific value. This is the default operator, so when you specify a value without any operator, the `$equal` operator is used under the hood:\n\n```\n// Find the Japanese movies\nawait Movie.find({country: {$equal: 'Japan'}});\n\n// Same as above, but in a short manner\nawait Movie.find({country: 'Japan'});\n```\n\n##### `$notEqual`\n\nUse the `$notEqual` operator to check whether the value of an attribute is different than a specific value:\n\n```\n// Find the non-Japanese movies\nawait Movie.find({country: {$notEqual: 'Japan'}});\n\n// Find the released movies\nawait Movie.find({year: {$notEqual: undefined}});\n```\n\n##### `$greaterThan`\n\nUse the `$greaterThan` operator to check whether the value of an attribute is greater than a specific value:\n\n```\n// Find the movies released after 2010\nawait Movie.find({year: {$greaterThan: 2010}});\n```\n\n##### `$greaterThanOrEqual`\n\nUse the `$greaterThanOrEqual` operator to check whether the value of an attribute is greater than or equal to a specific value:\n\n```\n// Find the movies released in or after 2010\nawait Movie.find({year: {$greaterThanOrEqual: 2010}});\n```\n\n##### `$lessThan`\n\nUse the `$lessThan` operator to check whether the value of an attribute is less than a specific value:\n\n```\n// Find the movies released before 2010\nawait Movie.find({year: {$lessThan: 2010}});\n```\n\n##### `$lessThanOrEqual`\n\nUse the `$lessThanOrEqual` operator to check whether the value of an attribute is less than or equal to a specific value:\n\n```\n// Find the movies released in or before 2010\nawait Movie.find({year: {$lessThanOrEqual: 2010}});\n```\n\n##### `$in`\n\nUse the `$in` operator to check whether the value of an attribute is equal to any value in the specified array:\n\n```\n// Find the movies that are Japanese or French\nawait Movie.find({country: {$in: ['Japan', 'France']}});\n\n// Find the movies that have any of the specified identifiers\nawait Movie.find({id: {$in: ['abc123', 'abc456', 'abc789']}});\n```\n\n##### Combining several operators\n\nYou can combine several operators to check whether the value of an attribute matches several criteria:\n\n```\n// Find the movies released after 2010 and before 2015\nawait Movie.find({year: {$greaterThan: 2010, $lessThan: 2015}});\n```\n\n#### String Operators\n\nA number of operators are dedicated to string attributes.\n\n##### `$includes`\n\nUse the `$includes` operator to check whether the value of a string attribute includes a specific string:\n\n```\n// Find the movies that have the string 'awesome' in their title\nawait Movie.find({title: {$includes: 'awesome'}});\n```\n\n##### `$startsWith`\n\nUse the `$startsWith` operator to check whether the value of a string attribute starts with a specific string:\n\n```\n// Find the movies that have their title starting with the string 'awesome'\nawait Movie.find({title: {$startsWith: 'awesome'}});\n```\n\n##### `$endsWith`\n\nUse the `$endsWith` operator to check whether the value of a string attribute ends with a specific string:\n\n```\n// Find the movies that have their title ending with the string 'awesome'\nawait Movie.find({title: {$endsWith: 'awesome'}});\n```\n\n##### `$matches`\n\nUse the `$matches` operator to check whether the value of a string attribute matches the specified [regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions):\n\n```\n// Find the movies that have a number in their title\nawait Movie.find({title: {$matches: /\\d/}});\n```\n\n#### Array Operators\n\nA number of operators are dedicated to array attributes.\n\n##### `$some`\n\nUse the `$some` operator to check whether an array attribute has an item equals to a specific value. This is the default operator for array attributes, so when you specify a value without any array operator, the `$some` operator is used under the hood:\n\n```\n// Find the movies that have the 'awesome' tag\nawait Movie.find({tags: {$some: 'awesome'}});\n\n// Same as above, but in a short manner\nawait Movie.find({tags: 'awesome'});\n```\n\n##### `$every`\n\nUse the `$every` operator to check whether an array attribute has all its items equals to a specific value:\n\n```\n// Find the movies that have all their tags equal to 'awesome'\nawait Movie.find({tags: {$every: 'awesome'}});\n```\n\n##### `$length`\n\nUse the `$length` operator to check whether an array attribute has a specific number of items:\n\n```\n// Find the movies that have three tags:\nawait Movie.find({tags: {$length: 3}});\n\n// Find the movies that don't have any tag:\nawait Movie.find({tags: {$length: 0}});\n```\n\n#### Logical Operators\n\nThe logical operators allows you to combine several subqueries.\n\n##### `$and`\n\nUse the `$and` operator to perform a logical **AND** operation on an array of subqueries and select the components that satisfy *all* the subqueries. Note that since **AND** is the implicit logical operation when you combine multiple attributes or operators, you will typically use the `$and` operator in combination with some other logical operators such as [`$or`](https://layrjs.com/docs/v2/reference/query#or) to avoid repetition.\n\n```\n// Find the Japanese drama movies\nawait Movie.find({$and: [{country: 'Japan'}, {genre: 'drama'}]});\n\n// Same as above, but in a short manner\nawait Movie.find({country: 'Japan', genre: 'drama'});\n\n// Find the movies released after 2010 and before 2015\nawait Movie.find({$and: [{year: {$greaterThan: 2010}}, {year: {$lessThan: 2015}}]});\n\n// Same as above, but in a short manner\nawait Movie.find({year: {$greaterThan: 2010, $lessThan: 2015}});\n\n// Find the Japanese movies released before 2010 or after 2015\nawait Movie.find({\n  $and: [\n    {country: 'Japan'},\n    {$or: [{year: {$lessThan: 2010}}, {year: {$greaterThan: 2015}}]}\n  ]\n});\n\n// Same as above, but we have to repeat the country to remove the $and operator\nawait Movie.find({\n  $or: [\n    {country: 'Japan', year: {$lessThan: 2010}},\n    {country: 'Japan', year: {$greaterThan: 2015}}\n  ]\n});\n```\n\n##### `$or`\n\nUse the `$or` operator to perform a logical **OR** operation on an array of subqueries and select the components that satisfy *at least* one of the subqueries.\n\n```\n// Find the movies that are either Japanese or a drama\nawait Movie.find({$or: [{country: 'Japan', {genre: 'drama'}]});\n\n// Find the movies released before 2010 or after 2015\nawait Movie.find({$or: [{year: {$lessThan: 2010}}, {year: {$greaterThan: 2015}}]});\n```\n\n##### `$nor`\n\nUse the `$nor` operator to perform a logical **NOR** operation on an array of subqueries and select the components that *fail all* the subqueries.\n\n```\n// Find the movies that are not Japanese and not a drama\nawait Movie.find({$nor: [{country: 'Japan', {genre: 'drama'}]});\n```\n\n##### `$not`\n\nUse the `$not` operator to invert the effect of an operator.\n\n```\n// Find the non-Japanese movies\nawait Movie.find({country: {$not: {$equal: 'Japan'}}});\n\n// Same as above, but in a short manner\nawait Movie.find({country: {$notEqual: 'Japan'}});\n\n// Find the movies that was not released in or after 2010\nawait Movie.find({year: {$not: {$greaterThanOrEqual: 2010}}});\n\n// Same as above, but in a short manner\nawait Movie.find({year: {$lessThan: 2010}});\n```\n\n#### Embedded Components\n\nWhen a query involves an [embedded component](https://layrjs.com/docs/v2/reference/embedded-component), wrap the attributes of the embedded component in an object:\n\n```\n// Find the movies that have a '16:9' aspect ratio\nawait Movie.find({details: {aspectRatio: '16:9'}});\n\n// Find the movies that have a '16:9' aspect ratio and are longer than 2 hours\nawait Movie.find({details: {aspectRatio: '16:9', duration: {$greaterThan: 120}}});\n```\n\n#### Referenced Components\n\nTo check whether a component holds a [reference to another component](https://layrjs.com/docs/v2/reference/component#referencing-components), you can specify an object representing the [primary identifier](https://layrjs.com/docs/v2/reference/primary-identifier-attribute) of the referenced component:\n\n```\n// Find the Tarantino's movies\nconst tarantino = await Director.get({slug: 'quentin-tarantino'});\nawait Movie.find({director: {id: tarantino.id}});\n```\n\nWherever you can specify a primary identifier, you can specify a component instead. So, the example above can be shortened as follows:\n\n```\n// Find the Tarantino's movies in a short manner\nconst tarantino = await Director.get({slug: 'quentin-tarantino'});\nawait Movie.find({director: tarantino});\n```\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/react-integration-4BgvR6gvTuqLdpNymyUYS1.immutable.md",
    "content": "### react-integration <badge type=\"primary\">module</badge> {#react-integration-module}\n\nProvides some React components, hooks, and decorators to simplify the use of [React](https://reactjs.org/) inside a Layr app.\n\n#### React Components\n\n##### `BrowserRootView()` <badge type=\"tertiary-outline\">react component</badge> {#browser-root-view-react-component}\n\nA React component providing sensible defaults for a web app.\n\nYou should use this component once at the top of your app.\n\nNote that if you use [Boostr](https://boostr.dev/) to manage your app development, this component will be automatically mounted, so you don't have to use it explicitly in your code.\n\nThe main point of this component is to provide the default behavior of high-level hooks such as [`useData()`](https://layrjs.com/docs/v2/reference/react-integration#use-data-react-hook) or [`useAction()`](https://layrjs.com/docs/v2/reference/react-integration#use-action-react-hook):\n\n- `useData()` will render `null` while the `getter()` function is running, and, in the case an error is thrown, a `<div>` containing an error message will be rendered.\n- `useAction()` will prevent the user from interacting with any UI element in the browser page while the `handler()` function is running, and, in the case an error is thrown, the browser's `alert()` function will be called to display the error message.\n\n**Example:**\n\nSee an example of use in the [`BrowserNavigatorView`](https://layrjs.com/docs/v2/reference/react-integration#browser-navigator-view-react-component) React component.\n##### `BrowserNavigatorView(props)` <badge type=\"tertiary-outline\">react component</badge> {#browser-navigator-view-react-component}\n\nA React component providing a [`BrowserNavigator`](https://layrjs.com/docs/v2/reference/browser-navigator#browser-navigator-class) to your app.\n\nYou should use this component once at the top of your app after the [`BrowserRootView`](https://layrjs.com/docs/v2/reference/react-integration#browser-root-view-react-component) component.\n\nNote that if you use [Boostr](https://boostr.dev/) to manage your app development, this component will be automatically mounted, so you don't have to use it explicitly in your code.\n\n**Parameters:**\n\n* `props`:\n  * `rootComponent`: The root Layr component of your app. Note that this Layr component should be [`Routable`](https://layrjs.com/docs/v2/reference/routable#routable-component-class).\n\n**Example:**\n\n```\n// JS\n\nimport React, {Fragment} from 'react';\nimport ReactDOM from 'react-dom';\nimport {Component} from '@layr/component';\nimport {Routable} from '@layr/routable';\nimport {BrowserRootView, BrowserNavigatorView, layout, page} from '@layr/react-integration';\n\nclass Application extends Routable(Component) {\n  // `@layout('/')` is a shortcut for `@wrapper('/') @view()`\n  @layout('/') static MainLayout({children}) {\n    return (\n      <>\n        <this.HomePage.Link>\n          <h1>My App</h1>\n        </this.HomePage.Link>\n\n        {children()} // Renders the subcomponents using this layout\n      </>\n    );\n  }\n\n  // `@page('[/]')` is a shortcut for `@route('[/]') @view()`\n  @page('[/]') static HomePage() {\n    return <p>Hello, World!</p>;\n  }\n}\n\n// Note that you don't need the following code when you use Boostr\nReactDOM.render(\n  <BrowserRootView>\n    <BrowserNavigatorView rootComponent={Application} />\n  </BrowserRootView>,\n  // Your `index.html` page should contain `<div id=\"root\"></div>`\n  document.getElementById('root')\n);\n```\n```\n// TS\n\nimport React, {Fragment} from 'react';\nimport ReactDOM from 'react-dom';\nimport {Component} from '@layr/component';\nimport {Routable} from '@layr/routable';\nimport {BrowserRootView, BrowserNavigatorView, layout, page} from '@layr/react-integration';\n\nclass Application extends Routable(Component) {\n  // `@layout('/')` is a shortcut for `@wrapper('/') @view()`\n  @layout('/') static MainLayout({children}: {children: () => any}) {\n    return (\n      <>\n        <this.HomePage.Link>\n          <h1>My App</h1>\n        </this.HomePage.Link>\n\n        {children()} // Renders the subcomponents using this layout\n      </>\n    );\n  }\n\n  // `@page('[/]')` is a shortcut for `@route('[/]') @view()`\n  @page('[/]') static HomePage() {\n    return <p>Hello, World!</p>;\n  }\n}\n\n// Note that you don't need the following code when you use Boostr\nReactDOM.render(\n  <BrowserRootView>\n    <BrowserNavigatorView rootComponent={Application} />\n  </BrowserRootView>,\n  // Your `index.html` page should contain `<div id=\"root\"></div>`\n  document.getElementById('root')\n);\n```\n\n##### `Customizer([props])` <badge type=\"tertiary-outline\">react component</badge> {#customizer-react-component}\n\nA React component allowing you to customize the behavior of high-level hooks such as [`useData()`](https://layrjs.com/docs/v2/reference/react-integration#use-data-react-hook) or [`useAction()`](https://layrjs.com/docs/v2/reference/react-integration#use-action-react-hook).\n\n**Parameters:**\n\n* `props`:\n  * `dataPlaceholder`: A function returning a React element (or `null`) that is rendered while the `getter()` function of the [`useData()`](https://layrjs.com/docs/v2/reference/react-integration#use-data-react-hook) hook is running. A typical use case is to render a spinner.\n  * `errorRenderer`: A function returning a React element (or `null`) that is rendered when the `getter()` function of the [`useData()`](https://layrjs.com/docs/v2/reference/react-integration#use-data-react-hook) hook throws an error. The `errorRenderer()` function receives the error as first parameter. A typical use case is to render an error message.\n  * `actionWrapper`: An asynchronous function allowing you to wrap the `handler()` function of the [`useAction()`](https://layrjs.com/docs/v2/reference/react-integration#use-data-react-hook) hook. The `actionWrapper()` function receives the `handler()` function as first parameter, should execute it, and return its result. A typical use case is to lock the screen while the `handler()` function is running so the user cannot interact with any UI element.\n  * `errorNotifier`: An asynchronous function that is executed when the `handler()` function of the [`useAction()`](https://layrjs.com/docs/v2/reference/react-integration#use-data-react-hook) hook throws an error. The `errorNotifier()` function receives the error as first parameter. A typical use case is to display an error alert dialog.\n\n**Example:**\n\n```\n<Customizer\n  dataPlaceholder={() => {\n    // Renders a custom `LoadingSpinner` component\n    return <LoadingSpinner />;\n  }}\n  errorRenderer={(error) => {\n    // Renders a custom `ErrorMessage` component\n    return <ErrorMessage>{error}</ErrorMessage>;\n  }}\n  actionWrapper={async (actionHandler, args) => {\n    // Do whatever you want here (e.g., custom screen locking)\n    try {\n      return await actionHandler(...args);\n    } finally {\n      // Do whatever you want here (e.g., custom screen unlocking)\n    }\n  }}\n  errorNotifier={async (error) => {\n    // Calls a custom `alert()` asynchronous function\n    await alert(error.message);\n  }}\n>\n  <YourChildComponent />\n</Customizer>\n```\n\n#### High-Level Hooks\n\n##### `useNavigator()` <badge type=\"tertiary-outline\">react hook</badge> {#use-navigator-react-hook}\n\nA hook allowing you to get the [`Navigator`](https://layrjs.com/docs/v2/reference/navigator#navigator-class) used in your app.\n\n**Returns:**\n\nA [`Navigator`](https://layrjs.com/docs/v2/reference/navigator#navigator-class) instance.\n\n**Example:**\n\n```\nimport {Component} from '@layr/component';\nimport {Routable} from '@layr/routable';\nimport React from 'react';\nimport {view, useNavigator} from '@layr/react-integration';\n\nimport logo from '../assets/app-logo.svg';\n\nclass Application extends Routable(Component) {\n  // ...\n\n  @view() static LogoView() {\n    const navigator = useNavigator();\n\n    return <img src={logo} onClick={() => { navigator.navigate('/); }} />;\n  }\n}\n```\n\n##### `useData(getter, renderer, [dependencies], [options])` <badge type=\"tertiary-outline\">react hook</badge> {#use-data-react-hook}\n\nA convenience hook for loading data asynchronously and rendering a React element using the loaded data.\n\nThe `getter()` asynchronous function is called when the React component is rendered for the first time and when a change is detected in its `dependencies`.\n\nWhile the `getter()` function is running, the `useData()` hook returns the result of the nearest `dataPlaceholder()` function, which can be defined in any parent component thanks to the [`Customizer`](https://layrjs.com/docs/v2/reference/react-integration#customizer-react-component) component.\n\nOnce the `getter()` function is executed, the `useData()` hook returns the result of the `renderer()` function which is called with the result of the `getter()` function as first parameter.\n\nIf an error occurs during the `getter()` function execution, the `useData()` hook returns the result of the nearest `errorRenderer()` function, which can be defined in any parent component thanks to the [`Customizer`](https://layrjs.com/docs/v2/reference/react-integration#customizer-react-component) component.\n\n**Parameters:**\n\n* `getter`: An asynchronous function for loading data.\n* `renderer`: A function which is called with the result of the `getter()` function as first parameter and a `refresh()` function as second parameter. You can call the `refresh()` function to force the re-execution of the `getter()` function. The `renderer()` function should return a React element (or `null`).\n* `dependencies`: An array of values on which the `getter()` function depends (default: `[]`).\n* `options`:\n  * `dataPlaceholder`: A custom `dataPlaceholder()` function.\n  * `errorRenderer`: A custom `errorRenderer()` function.\n\n**Returns:**\n\nA React element (or `null`).\n\n**Example:**\n\n```\nimport {Component} from '@layr/component';\nimport React from 'react';\nimport {view, useData} from '@layr/react-integration';\n\nclass Article extends Component {\n  // ...\n\n  @view() static List() {\n    return useData(\n      async () => {\n        // Return some articles from the backend\n      },\n\n      (articles) => {\n        return articles.map((article) => (\n          <div key={article.id}>{article.title}</div>\n        ));\n      }\n    );\n  }\n}\n```\n\n##### `useAction(handler, [dependencies], [options])` <badge type=\"tertiary-outline\">react hook</badge> {#use-action-react-hook}\n\nA convenience hook for executing some asynchronous actions.\n\nThe specified `handler()` asynchronous function is wrapped so that:\n\n- When running, the screen is locked to prevent the user from interacting with any UI element. You can customize the screen locking mechanism in any parent component thanks to the [Customizer's `actionWrapper()`](https://layrjs.com/docs/v2/reference/react-integration#customizer-react-component) prop.\n- In case an error is thrown, an error alert dialog is displayed. You can customize the error alert dialog in any parent component thanks to the [Customizer's `errorNotifier()`](https://layrjs.com/docs/v2/reference/react-integration#customizer-react-component) prop.\n\n**Parameters:**\n\n* `handler`: An asynchronous function implementing the action.\n* `dependencies`: An array of values on which the `handler()` function depends (default: `[]`).\n* `options`:\n  * `actionWrapper`: A custom `actionWrapper()` function.\n  * `errorNotifier`: A custom `errorNotifier()` function.\n\n**Returns:**\n\nAn asynchronous function wrapping the specified `handler()`.\n\n**Example:**\n\n```\nimport {Component} from '@layr/component';\nimport React from 'react';\nimport {view, useAction} from '@layr/react-integration';\n\nclass Article extends Component {\n  // ...\n\n  @view() EditView() {\n    const save = useAction(async () => {\n      // Save the edited article to the backend\n    });\n\n    return (\n      <form\n        onSubmit={(event) => {\n          event.preventDefault();\n          save();\n        }}\n      >\n        <div>\n          Implement your form fields here.\n        </div>\n\n        <div>\n          <button type=\"submit\">Save</button>\n        </div>\n      </form>\n    );\n  }\n}\n```\n\n##### `useObserve(observable)` <badge type=\"tertiary-outline\">react hook</badge> {#use-observe-react-hook}\n\nMakes a view dependent of an [observable](https://layrjs.com/docs/v2/reference/observable#observable-type) so the view is automatically re-rendered when the observable changes.\n\n**Parameters:**\n\n* `observable`: An [observable](https://layrjs.com/docs/v2/reference/observable#observable-type) object.\n\n**Example:**\n\n```\nimport {Component} from '@layr/component';\nimport {createObservable} from '@layr/observable';\nimport React from 'react';\nimport {view, useObserve} from '@layr/react-integration';\n\nconst observableArray = createObservable([]);\n\nclass MyComponent extends Component {\n  @view() static View() {\n    useObserve(observableArray);\n\n    return (\n      <div>\n        {`observableArray's length: ${observableArray.length}`}\n      </div>\n    );\n  }\n}\n\n// Changing `observableArray` will re-render `MyComponent.View`\nobservableArray.push('abc');\n```\n\n#### Decorators\n\n##### `@view()` <badge type=\"tertiary\">decorator</badge> {#view-decorator}\n\nDecorates a method of a Layr [component](https://layrjs.com/docs/v2/reference/component) so it be can used as a React component.\n\nLike any React component, the method can receive some properties as first parameter and return some React elements to render.\n\nThe decorator binds the method to a specific component, so when the method is executed by React (via, for example, a reference included in a [JSX expression](https://reactjs.org/docs/introducing-jsx.html)), it has access to the bound component through `this`.\n\nAlso, the decorator observes the attributes of the bound component, so when the value of an attribute changes, the React component is automatically re-rendered.\n\n**Example:**\n\n```\nimport {Component, attribute} from '@layr/component';\nimport React from 'react';\nimport ReactDOM from 'react-dom';\nimport {view} from '@layr/react-integration';\n\nclass Person extends Component {\n  @attribute('string') firstName = '';\n\n  @attribute('string') lastName = '';\n\n  @view() FullName() {\n    return <span>{`${this.firstName} ${this.fullName}`}</span>;\n  }\n}\n\nconst person = new Person({firstName: 'Alan', lastName: 'Turing'});\n\nReactDOM.render(<person.FullName />, document.getElementById('root'));\n```\n\n##### `@page(pattern, [options])` <badge type=\"tertiary\">decorator</badge> {#page-decorator}\n\nA convenience decorator that combines the [`@route()`](https://layrjs.com/docs/v2/reference/routable#route-decorator) and [`@view()`](https://layrjs.com/docs/v2/reference/react-integration#view-decorator) decorators.\n\nTypically, you should use this decorator to implement the pages of your app.\n\n**Parameters:**\n\n* `pattern`: The canonical [URL pattern](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type) of the route.\n* `options`: An object specifying the options to pass to the `Route`'s [constructor](https://layrjs.com/docs/v2/reference/addressable#constructor) when the route is created.\n\n**Example:**\n\nSee an example of use in the [`BrowserNavigatorView`](https://layrjs.com/docs/v2/reference/react-integration#browser-navigator-view-react-component) React component.\n##### `@layout(pattern, [options])` <badge type=\"tertiary\">decorator</badge> {#layout-decorator}\n\nA convenience decorator that combines the [`@wrapper()`](https://layrjs.com/docs/v2/reference/routable#wrapper-decorator) and [`@view()`](https://layrjs.com/docs/v2/reference/react-integration#view-decorator) decorators.\n\nTypically, you should use this decorator to implement the layouts of your app.\n\n**Parameters:**\n\n* `pattern`: The canonical [URL pattern](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type) of the wrapper.\n* `options`: An object specifying the options to pass to the `Wrapper`'s [constructor](https://layrjs.com/docs/v2/reference/addressable#constructor) when the wrapper is created.\n\n**Example:**\n\nSee an example of use in the [`BrowserNavigatorView`](https://layrjs.com/docs/v2/reference/react-integration#browser-navigator-view-react-component) React component.\n#### Low-Level Hooks\n\n##### `useAsyncCallback(asyncCallback, [dependencies])` <badge type=\"tertiary-outline\">react hook</badge> {#use-async-callback-react-hook}\n\nAllows you to define an asynchronous callback and keep track of its execution.\n\nPlays the same role as the React built-in [`useCallback()`](https://reactjs.org/docs/hooks-reference.html#usecallback) hook but works with asynchronous callbacks.\n\n**Parameters:**\n\n* `asyncCallback`: An asynchronous callback.\n* `dependencies`: An array of values on which the asynchronous callback depends (default: `[]`).\n\n**Returns:**\n\nAn array of the shape `[trackedCallback, isExecuting, error, result]` where `trackedCallback` is a function that you can call to execute the asynchronous callback, `isExecuting` is a boolean indicating whether the asynchronous callback is being executed, `error` is the error thrown by the asynchronous callback in case of failed execution, and `result` is the value returned by the asynchronous callback in case of succeeded execution.\n\n**Example:**\n\n```\nimport {Component} from '@layr/component';\nimport React from 'react';\nimport {view, useAsyncCallback} from '@layr/react-integration';\n\nclass Article extends Component {\n  @view() UpvoteButton() {\n    const [handleUpvote, isUpvoting, upvotingError] = useAsyncCallback(async () => {\n      await this.upvote();\n    });\n\n    return (\n      <div>\n        <button onClick={handleUpvote} disabled={isUpvoting}>Upvote</button>\n        {upvotingError && ' An error occurred while upvoting the article.'}\n      </div>\n    );\n  }\n}\n```\n\n##### `useAsyncMemo(asyncFunc, [dependencies])` <badge type=\"tertiary-outline\">react hook</badge> {#use-async-memo-react-hook}\n\nMemoizes the result of an asynchronous function execution and provides a \"recompute function\" that you can call to recompute the memoized result.\n\nThe asynchronous function is executed one time when the React component is rendered for the first time, and each time a dependency is changed or the \"recompute function\" is called.\n\nPlays the same role as the React built-in [`useMemo()`](https://reactjs.org/docs/hooks-reference.html#usememo) hook but works with asynchronous functions and allows to recompute the memoized result.\n\n**Parameters:**\n\n* `asyncFunc`: An asynchronous function to compute the memoized result.\n* `dependencies`: An array of values on which the memoized result depends (default: `[]`, which means that the memoized result will be recomputed only when the \"recompute function\" is called).\n\n**Returns:**\n\nAn array of the shape `[memoizedResult, isExecuting, error, recompute]` where `memoizedResult` is the result returned by the asynchronous function in case of succeeded execution, `isExecuting` is a boolean indicating whether the asynchronous function is being executed, `error` is the error thrown by the asynchronous function in case of failed execution, and `recompute` is a function that you can call to recompute the memoized result.\n\n**Example:**\n\n```\nimport {Component} from '@layr/component';\nimport React from 'react';\nimport {view, useAsyncMemo} from '@layr/react-integration';\n\nclass Article extends Component {\n  // ...\n\n  @view() static List() {\n    const [articles, isLoading, loadingError, retryLoading] = useAsyncMemo(\n      async () => {\n        // Return some articles from the backend\n      }\n    );\n\n    if (isLoading) {\n      return <div>Loading the articles...</div>;\n    }\n\n    if (loadingError) {\n      return (\n        <div>\n          An error occurred while loading the articles.\n          <button onClick={retryLoading}>Retry</button>\n        </div>\n      );\n    }\n\n    return articles.map((article) => (\n      <div key={article.id}>{article.title}</div>\n    ));\n  }\n}\n```\n\n##### `useRecomputableMemo(func, [dependencies])` <badge type=\"tertiary-outline\">react hook</badge> {#use-recomputable-memo-react-hook}\n\nMemoizes the result of a function execution and provides a \"recompute function\" that you can call to recompute the memoized result.\n\nThe function is executed one time when the React component is rendered for the first time, and each time a dependency is changed or the \"recompute function\" is called.\n\nPlays the same role as the React built-in [`useMemo()`](https://reactjs.org/docs/hooks-reference.html#usememo) hook but with the extra ability to recompute the memoized result.\n\n**Parameters:**\n\n* `func`: A function to compute the memoized result.\n* `dependencies`: An array of values on which the memoized result depends (default: `[]`, which means that the memoized result will be recomputed only when the \"recompute function\" is called).\n\n**Returns:**\n\nAn array of the shape `[memoizedResult, recompute]` where `memoizedResult` is the result of the function execution, and `recompute` is a function that you can call to recompute the memoized result.\n\n**Example:**\n\n```\nimport {Component, provide} from '@layr/component';\nimport React, {useCallback} from 'react';\nimport {view, useRecomputableMemo} from '@layr/react-integration';\n\nclass Article extends Component {\n  // ...\n}\n\nclass Blog extends Component {\n  @provide() static Article = Article;\n\n  @view() static CreateArticleView() {\n    const [article, resetArticle] = useRecomputableMemo(() => new Article());\n\n    const createArticle = useCallback(async () => {\n      // Save the created article to the backend\n      resetArticle();\n    }, [article]);\n\n    return (\n      <div>\n        <article.CreateForm onSubmit={createArticle} />\n      </div>\n    );\n  }\n}\n```\n\n##### `useAsyncCall(asyncFunc, [dependencies])` <badge type=\"tertiary-outline\">react hook</badge> {#use-async-call-react-hook}\n\nAllows you to call an asynchronous function and keep track of its execution.\n\nThe function is executed one time when the React component is rendered for the first time, and each time a dependency is changed or the \"recall function\" is called.\n\n**Parameters:**\n\n* `asyncFunc`: The asynchronous function to call.\n* `dependencies`: An array of values on which the asynchronous function depends (default: `[]`, which means that the asynchronous will be recalled only when the \"recall function\" is called).\n\n**Returns:**\n\nAn array of the shape `[isExecuting, error, recall]` where `isExecuting` is a boolean indicating whether the asynchronous function is being executed, `error` is the error thrown by the asynchronous function in case of failed execution, and `recall` is a function that you can call to recall the asynchronous function.\n\n**Example:**\n\n```\n// JS\n\nimport {Component, provide, attribute} from '@layr/component';\nimport React from 'react';\nimport {view, useAsyncCall} from '@layr/react-integration';\n\nclass Article extends Component {\n  // ...\n}\n\nclass Blog extends Component {\n  @provide() static Article = Article;\n\n  @attribute('Article[]?') static loadedArticles;\n\n  @view() static View() {\n    const [isLoading, loadingError, retryLoading] = useAsyncCall(\n      async () => {\n        this.loadedArticles = await this.Article.find();\n      }\n    );\n\n    if (isLoading) {\n      return <div>Loading the articles...</div>;\n    }\n\n    if (loadingError) {\n      return (\n        <div>\n          An error occurred while loading the articles.\n          <button onClick={retryLoading}>Retry</button>\n        </div>\n      );\n    }\n\n    return this.loadedArticles.map((article) => (\n      <div key={article.id}>{article.title}</div>\n    ));\n  }\n}\n```\n```\n// TS\n\nimport {Component, provide, attribute} from '@layr/component';\nimport React from 'react';\nimport {view, useAsyncCall} from '@layr/react-integration';\n\nclass Article extends Component {\n  // ...\n}\n\nclass Blog extends Component {\n  @provide() static Article = Article;\n\n  @attribute('Article[]?') static loadedArticles?: Article[];\n\n  @view() static View() {\n    const [isLoading, loadingError, retryLoading] = useAsyncCall(\n      async () => {\n        this.loadedArticles = await this.Article.find();\n      }\n    );\n\n    if (isLoading) {\n      return <div>Loading the articles...</div>;\n    }\n\n    if (loadingError) {\n      return (\n        <div>\n          An error occurred while loading the articles.\n          <button onClick={retryLoading}>Retry</button>\n        </div>\n      );\n    }\n\n    return this.loadedArticles!.map((article) => (\n      <div key={article.id}>{article.title}</div>\n    ));\n  }\n}\n```\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/role-3zJ3w6whE8RucN9kAhS5Hv.immutable.md",
    "content": "### Role <badge type=\"primary\">class</badge> {#role-class}\n\nRepresents a role in a [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class) class or prototype.\n\nA role is composed of:\n\n- A name.\n- A parent which should be a [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class) class or prototype.\n- A resolver which should be a function returning a boolean indicating whether a user has the corresponding role.\n\n#### Usage\n\nTypically, you create a `Role` by using the [`@role()`](https://layrjs.com/docs/v2/reference/with-roles#role-decorator) decorator.\n\nSee an example of use in the [`WithRoles()`](https://layrjs.com/docs/v2/reference/with-roles#with-roles-mixin) mixin.\n\n#### Creation\n\n##### `new Role(name, parent, resolver)` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates an instance of [`Role`](https://layrjs.com/docs/v2/reference/role).\n\nTypically, instead of using this constructor, you would rather use the [`@role()`](https://layrjs.com/docs/v2/reference/with-roles#role-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the role.\n* `parent`: The parent of the role which should be a [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class) class or prototype.\n* `resolver`: A function that should return a boolean indicating whether a user has the corresponding role. The function can be asynchronous and is called with the current class or instance as `this` context.\n\n**Returns:**\n\nThe [`Role`](https://layrjs.com/docs/v2/reference/role) instance that was created.\n\n**Example:**\n\n```\nconst role = new Role('admin', Article, function () {\n // ...\n});\n```\n\n#### Methods\n\n##### `getName()` <badge type=\"secondary-outline\">instance method</badge> {#get-name-instance-method}\n\nReturns the name of the role.\n\n**Returns:**\n\nA string.\n\n##### `getParent()` <badge type=\"secondary-outline\">instance method</badge> {#get-parent-instance-method}\n\nReturns the parent of the role.\n\n**Returns:**\n\nA [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class) class or instance.\n\n##### `getResolver()` <badge type=\"secondary-outline\">instance method</badge> {#get-resolver-instance-method}\n\nReturns the resolver function of the role.\n\n**Returns:**\n\nA function.\n\n##### `resolve()` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">possibly async</badge> {#resolve-instance-method}\n\nResolves the role by calling its resolver function.\n\nThe resolver function is called with the role's parent as `this` context.\n\nOnce a role has been resolved, the result is cached, so the resolver function is called only one time.\n\n**Returns:**\n\nA boolean.\n\n#### Utilities\n\n##### `isRoleInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-role-instance-function}\n\nReturns whether the specified value is a [`Role`](https://layrjs.com/docs/v2/reference/role) instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `assertIsRoleInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#assert-is-role-instance-function}\n\nThrows an error if the specified value is not a [`Role`](https://layrjs.com/docs/v2/reference/role) instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/routable-7zjaksbkNa8sbNfqNnEKUu.immutable.md",
    "content": "### Routable() <badge type=\"primary\">mixin</badge> {#routable-mixin}\n\nExtends a [`Component`](https://layrjs.com/docs/v2/reference/component) class with some routing capabilities.\n\n#### Usage\n\nCall `Routable()` with a [`Component`](https://layrjs.com/docs/v2/reference/component) class to construct a [`RoutableComponent`](https://layrjs.com/docs/v2/reference/routable#routable-component-class) class.\n\nThen, you can define some routes or wrappers into this class by using the [`@route()`](https://layrjs.com/docs/v2/reference/routable#route-decorator) or [`@wrapper()`](https://layrjs.com/docs/v2/reference/routable#wrapper-decorator) decorators.\n\nSee an example of use in the [`BrowserNavigatorView`](https://layrjs.com/docs/v2/reference/react-integration#browser-navigator-view-react-component) React component.\n\n### RoutableComponent <badge type=\"primary\">class</badge> {#routable-component-class}\n\n*Inherits from [`Component`](https://layrjs.com/docs/v2/reference/component).*\n\nA `RoutableComponent` class is constructed by calling the `Routable()` mixin ([see above](https://layrjs.com/docs/v2/reference/routable#routable-mixin)).\n\n#### Component Methods\n\nSee the methods that are inherited from the [`Component`](https://layrjs.com/docs/v2/reference/component#creation) class.\n\n#### Navigator\n\n##### `getNavigator()` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#get-navigator-dual-method}\n\nReturns the navigator in which the routable component is registered. If the routable component is not registered in a navigator, an error is thrown.\n\n**Returns:**\n\nA [`Navigator`](https://layrjs.com/docs/v2/reference/navigator) instance.\n\n**Example:**\n\n```\nArticle.getNavigator(); // => navigator instance\n```\n\n#### Routes\n\n##### `getRoute(name)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#get-route-dual-method}\n\nGets a route. If there is no route with the specified name, an error is thrown.\n\n**Parameters:**\n\n* `name`: The name of the route to get.\n\n**Returns:**\n\nA [Route](https://layrjs.com/docs/v2/reference/route) instance.\n\n**Example:**\n\n```\nArticle.getRoute('View'); // A route instance\n```\n\n##### `hasRoute(name)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#has-route-dual-method}\n\nReturns whether the routable component has a route with the specified name.\n\n**Parameters:**\n\n* `name`: The name of the route to check.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nArticle.hasRoute('View'); // => true\n```\n\n##### `setRoute(name, pattern, [options])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#set-route-dual-method}\n\nSets a route for a routable component class or instances.\n\nTypically, instead of using this method, you would rather use the [`@route()`](https://layrjs.com/docs/v2/reference/routable#route-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the route.\n* `pattern`: The canonical [URL pattern](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type) of the route.\n* `options`: An optional object specifying the options to pass to the `Route`'s [constructor](https://layrjs.com/docs/v2/reference/addressable#constructor) when the route is created.\n\n**Returns:**\n\nThe [Route](https://layrjs.com/docs/v2/reference/route) instance that was created.\n\n**Example:**\n\n```\nArticle.setRoute('View', '/articles', {parameters: {page: 'number?'});\n\nArticle.prototype.setRoute('View', '/articles/:id', {parameters: {showDetails: 'boolean?'}});\n```\n\n##### `findRouteByURL(url)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#find-route-by-url-dual-method}\n\nFinds the first route that matches the specified URL.\n\n**Parameters:**\n\n* `url`: A string or a [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) object.\n\n**Returns:**\n\nWhen a route is found, returns an object of the shape `{route, identifiers, params}` where `route` is the [route](https://layrjs.com/docs/v2/reference/route) that was found, `identifiers` is a plain object containing the value of some [component identifier attributes](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type), and `params` is a plain object containing the value of some [URL parameters](https://layrjs.com/docs/v2/reference/addressable#url-parameters-type). If no routes were found, returns `undefined`.\n\n**Example:**\n\n```\nconst result = Article.prototype.findRouteByURL('/articles/abc123?showDetails=1');\n\nresult.route; // => A route instance\nresult.identifiers; // => {id: 'abc123'}\nresult.params; // => {showDetails: true}\n```\n\n#### Wrappers\n\n##### `getWrapper(name)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#get-wrapper-dual-method}\n\nGets a wrapper. If there is no wrapper with the specified name, an error is thrown.\n\n**Parameters:**\n\n* `name`: The name of the wrapper to get.\n\n**Returns:**\n\nA [Wrapper](https://layrjs.com/docs/v2/reference/wrapper) instance.\n\n**Example:**\n\n```\nArticle.getWrapper('Layout'); => A wrapper instance\n```\n\n##### `hasWrapper(name)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#has-wrapper-dual-method}\n\nReturns whether the routable component has a wrapper with the specified name.\n\n**Parameters:**\n\n* `name`: The name of the wrapper to check.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nArticle.hasWrapper('Layout'); // => true\n```\n\n##### `setWrapper(name, pattern, [options])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#set-wrapper-dual-method}\n\nSets a wrapper for a routable component class or instances.\n\nTypically, instead of using this method, you would rather use the [`@wrapper()`](https://layrjs.com/docs/v2/reference/routable#wrapper-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the wrapper.\n* `pattern`: The canonical [URL pattern](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type) of the wrapper.\n* `options`: An optional object specifying the options to pass to the `Wrapper`'s [constructor](https://layrjs.com/docs/v2/reference/addressable#constructor) when the wrapper is created.\n\n**Returns:**\n\nThe [Wrapper](https://layrjs.com/docs/v2/reference/wrapper) instance that was created.\n\n**Example:**\n\n```\nArticle.setWrapper('Layout', '/articles');\n\nArticle.prototype.setWrapper('View', '[/articles]/:id');\n```\n\n##### `findWrapperByPath(path)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#find-wrapper-by-path-dual-method}\n\nFinds the first wrapper that matches the specified path.\n\n**Parameters:**\n\n* `path`: A string representing a path.\n\n**Returns:**\n\nWhen a wrapper is found, returns an object of the shape `{wrapper, identifiers}` where `wrapper` is the [wrapper](https://layrjs.com/docs/v2/reference/wrapper) that was found and `identifiers` is a plain object containing the value of some [component identifier attributes](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type). If no wrappers were found, returns `undefined`.\n\n**Example:**\n\n```\nconst result = Article.prototype.findWrapperByPath('/articles/abc123');\n\nresult.wrapper; // => A wrapper instance\nresult.identifiers; // => {id: 'abc123'}\n```\n\n#### Observability\n\nSee the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) class.\n\n#### Decorators\n\n##### `@route(pattern, [options])` <badge type=\"tertiary\">decorator</badge> {#route-decorator}\n\nDefines a [route](https://layrjs.com/docs/v2/reference/route) for a static or instance method in a [routable component](https://layrjs.com/docs/v2/reference/routable#routable-component-class).\n\n**Parameters:**\n\n* `pattern`: The canonical [URL pattern](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type) of the route.\n* `options`: An object specifying the options to pass to the `Route`'s [constructor](https://layrjs.com/docs/v2/reference/addressable#constructor) when the route is created.\n\n**Shortcut functions:**\n\nIn addition to defining a route, the decorator adds some shortcut functions to the decorated method so that you can interact with the route more easily.\n\nFor example, if you define a `route` for a `View()` method you automatically get the following functions:\n\n- `View.matchURL(url)` is the equivalent of [`route.matchURL(url)`](https://layrjs.com/docs/v2/reference/addressable#match-url-instance-method).\n- `View.generateURL([params], [options])` is the equivalent of [`route.generateURL(component, [params], [options])`](https://layrjs.com/docs/v2/reference/addressable#generate-url-instance-method) where `component` is the [routable component](https://layrjs.com/docs/v2/reference/routable#routable-component-class) associated with the `View()` method.\n\nIf the defined `route` is controlled by a [`navigator`](https://layrjs.com/docs/v2/reference/navigator), you also get the following shortcut functions:\n\n- `View.navigate([params], [options])` is the equivalent of [`navigator.navigate(url, options)`](https://layrjs.com/docs/v2/reference/navigator#navigate-instance-method) where `url` is generated by calling `View.generateURL([params], [options])`.\n- `View.redirect([params], [options])` is the equivalent of [`navigator.redirect(url, options)`](https://layrjs.com/docs/v2/reference/navigator#redirect-instance-method) where `url` is generated by calling `View.generateURL([params], [options])`.\n- `View.reload([params], [options])` is the equivalent of [`navigator.reload(url)`](https://layrjs.com/docs/v2/reference/navigator#reload-instance-method) where `url` is generated by calling `View.generateURL([params], [options])`.\n- `View.isActive()` returns a boolean indicating whether the `route`'s URL (generated by calling `View.generateURL()`) matches the current `navigator`'s URL.\n\nLastly, if the defined `route` is controlled by a [`navigator`](https://layrjs.com/docs/v2/reference/navigator) that is created by using the [`useBrowserNavigator()`](https://layrjs.com/docs/v2/reference/react-integration#use-browser-navigator-react-hook) React hook, you also get the following shortcut React component:\n\n- `View.Link({params, hash, ...props})` is the equivalent of [`navigator.Link({to, ...props})`](https://layrjs.com/docs/v2/reference/browser-navigator#link-instance-method) where `to` is generated by calling `View.generateURL(params, {hash})`.\n\n**Example:**\n\nSee an example of use in the [`BrowserNavigatorView`](https://layrjs.com/docs/v2/reference/react-integration#browser-navigator-view-react-component) React component.\n##### `@wrapper(pattern, [options])` <badge type=\"tertiary\">decorator</badge> {#wrapper-decorator}\n\nDefines a [wrapper](https://layrjs.com/docs/v2/reference/wrapper) for a static or instance method in a [routable component](https://layrjs.com/docs/v2/reference/routable#routable-component-class).\n\n**Parameters:**\n\n* `pattern`: The canonical [URL pattern](https://layrjs.com/docs/v2/reference/addressable#url-pattern-type) of the wrapper.\n* `options`: An object specifying the options to pass to the `Wrapper`'s [constructor](https://layrjs.com/docs/v2/reference/addressable#constructor) when the wrapper is created.\n\n**Example:**\n\nSee an example of use in the [`BrowserNavigatorView`](https://layrjs.com/docs/v2/reference/react-integration#browser-navigator-view-react-component) React component.\n#### Utilities\n\n##### `isRoutableClass(value)` <badge type=\"tertiary-outline\">function</badge> {#is-routable-class-function}\n\nReturns whether the specified value is a routable component class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isRoutableInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-routable-instance-function}\n\nReturns whether the specified value is a routable component class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isRoutableClassOrInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-routable-class-or-instance-function}\n\nReturns whether the specified value is a routable component class or instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `assertIsRoutableClass(value)` <badge type=\"tertiary-outline\">function</badge> {#assert-is-routable-class-function}\n\nThrows an error if the specified value is not a routable component class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n##### `assertIsRoutableInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#assert-is-routable-instance-function}\n\nThrows an error if the specified value is not a routable component instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n##### `assertIsRoutableClassOrInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#assert-is-routable-class-or-instance-function}\n\nThrows an error if the specified value is not a routable component class or instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/route-3O1kMMGl4yZc9LGxH38Z3G.immutable.md",
    "content": "### Route <badge type=\"primary\">class</badge> {#route-class}\n\n*Inherits from [`Addressable`](https://layrjs.com/docs/v2/reference/addressable).*\n\nRepresents a route in a [routable component](https://layrjs.com/docs/v2/reference/routable#routable-component-class).\n\n#### Usage\n\nTypically, you create a `Route` and associate it to a routable component by using the [`@route()`](https://layrjs.com/docs/v2/reference/routable#route-decorator) decorator.\n\nSee an example of use in the [`BrowserNavigatorView`](https://layrjs.com/docs/v2/reference/react-integration#browser-navigator-view-react-component) React component.\n\n#### Creation\n\nSee the constructor that is inherited from the [`Addressable`](https://layrjs.com/docs/v2/reference/addressable#constructor) class.\n\n#### Methods\n\nSee the methods that are inherited from the [`Addressable`](https://layrjs.com/docs/v2/reference/addressable#basic-methods) class.\n\n#### Types\n\nSee the types that are related to the [`Addressable`](https://layrjs.com/docs/v2/reference/addressable#types) class.\n\n#### Utilities\n\n##### `isRouteClass(value)` <badge type=\"tertiary-outline\">function</badge> {#is-route-class-function}\n\nReturns whether the specified value is a [`Route`](https://layrjs.com/docs/v2/reference/route) class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isRouteInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-route-instance-function}\n\nReturns whether the specified value is a [`Route`](https://layrjs.com/docs/v2/reference/route) instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/sanitizer-3hc34yGhzbK1cupoMtP6Dv.immutable.md",
    "content": "### Sanitizer <badge type=\"primary\">class</badge> {#sanitizer-class}\n\nA class to handle the sanitization of the component attributes.\n\n#### Usage\n\nYou shouldn't have to create a `Sanitizer` instance directly. Instead, when you define an attribute (using a decorator such as [`@attribute()`](https://layrjs.com/docs/v2/reference/component#attribute-decorator)), you can invoke some [built-in sanitizer builders](https://layrjs.com/docs/v2/reference/sanitizer#built-in-sanitizer-builders) or specify your own [custom sanitization functions](https://layrjs.com/docs/v2/reference/sanitizer#custom-sanitization-functions) that will be automatically transformed into `Sanitizer` instances.\n\n**Example:**\n\n```\n// JS\n\nimport {Component, attribute, sanitizers} from '@layr/component';\n\nconst {trim, compact} = sanitizers;\n\nclass Movie extends Component {\n  // An attribute of type 'string' that is automatically trimmed\n  @attribute('string', {sanitizers: [trim()]}) title;\n\n  // An array attribute for storing non-empty strings\n  @attribute('string[]', {sanitizers: [compact()], items: {sanitizers: [trim()]}})\n  tags = [];\n}\n```\n\n```\n// TS\n\nimport {Component, attribute, sanitizers} from '@layr/component';\n\nconst {trim, compact} = sanitizers;\n\nclass Movie extends Component {\n  // An attribute of type 'string' that is automatically trimmed\n  @attribute('string', {sanitizers: [trim()]}) title!: string;\n\n  // An array attribute for storing non-empty strings\n  @attribute('string[]', {sanitizers: [compact()], items: {sanitizers: [trim()]}})\n  tags: string[] = [];\n}\n```\n\nIn case you want to access the `Sanitizer` instances that were created under the hood, you can do the following:\n\n```\nconst movie = new Movie({ ... });\n\nmovie.getAttribute('title').getValueType().getSanitizers();\n// => [trimSanitizer]\n\nmovie.getAttribute('tags').getValueType().getSanitizers();\n// => [compactSanitizer]\n\nmovie.getAttribute('tags').getValueType().getItemType().getSanitizers();\n// => [trimSanitizer]\n```\n\n#### Built-In Sanitizer Builders\n\nLayr provides some sanitizer builders that can be used when you define your component attributes. See an [example of use](https://layrjs.com/docs/v2/reference/sanitizer#usage) above.\n\n##### Strings\n\nThe following sanitizer builder can be used to sanitize strings:\n\n* `trim()`: Removes leading and trailing whitespace from the string.\n\n##### Arrays\n\nThe following sanitizer builder can be used to sanitize arrays:\n\n* `compact()`: Removes all falsey values from the array. The values `false`, `null`, `0`, `''`, `undefined`, and `NaN` are falsey.\n\n#### Custom Sanitization Functions\n\nIn addition to the [built-in sanitizer builders](https://layrjs.com/docs/v2/reference/sanitizer#built-in-sanitizer-builders), you can sanitize your component attributes with your own custom sanitization functions.\n\nA custom sanitization function takes a value as first parameter and returns a new value that is the result of the sanitization.\n\n**Example:**\n\n```\n// JS\n\nimport {Component, attribute} from '@layr/component';\n\nclass Integer extends Component {\n  // Ensures that the value is an integer\n  @attribute('number', {sanitizers: [(value) => Math.round(value)]}) value;\n}\n```\n\n```\n// TS\n\nimport {Component, attribute} from '@layr/component';\n\nclass Integer extends Component {\n  // Ensures that the value is an integer\n  @attribute('number', {sanitizers: [(value) => Math.round(value)]}) value!: number;\n}\n```\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/secondary-identifier-attribute-27lBtqHBopmgIAWwmeoLBg.immutable.md",
    "content": "### SecondaryIdentifierAttribute <badge type=\"primary\">class</badge> {#secondary-identifier-attribute-class}\n\n*Inherits from [`IdentifierAttribute`](https://layrjs.com/docs/v2/reference/identifier-attribute).*\n\nA `SecondaryIdentifierAttribute` is a special kind of attribute that uniquely identify a [Component](https://layrjs.com/docs/v2/reference/component) instance.\n\nContrary to a [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute), you can define more than one `SecondaryIdentifierAttribute` in a `Component`.\n\nAnother difference with a `PrimaryIdentifierAttribute` is that a `SecondaryIdentifierAttribute` value is mutable (i.e., it can change over time).\n\n#### Usage\n\nTypically, you create a `SecondaryIdentifierAttribute` and associate it to a component prototype using the [`@secondaryIdentifier()`](https://layrjs.com/docs/v2/reference/component#secondary-identifier-decorator) decorator.\n\nA common use case is a `User` component with an immutable primary identifier and a secondary identifier for the email address that can change over time:\n\n```\n// JS\n\nimport {Component, primaryIdentifier, secondaryIdentifier} from '@layr/component';\n\nclass User extends Component {\n  @primaryIdentifier() id;\n  @secondaryIdentifier() email;\n}\n```\n\n```\n// TS\n\nimport {Component, primaryIdentifier, secondaryIdentifier} from '@layr/component';\n\nclass User extends Component {\n  @primaryIdentifier() id!: string;\n  @secondaryIdentifier() email!: string;\n}\n```\n\nTo create a `User` instance, you would do something like:\n\n```\nconst user = new User({email: 'someone@domain.tld'});\n\nuser.id; // => 'ck41vli1z00013h5xx1esffyn'\nuser.email; // => 'someone@domain.tld'\n```\n\nNote that the primary identifier (`id`) was auto-generated, but we had to provide a value for the secondary identifier (`email`) because secondary identifiers cannot be `undefined` and they are not commonly auto-generated.\n\nLike previously mentioned, contrary to a primary identifier, the value of a secondary identifer can be changed:\n\n```\nuser.email = 'someone-else@domain.tld'; // Okay\nuser.id = 'ck2zrb1xs00013g5to1uimigb'; // Error\n```\n\n`SecondaryIdentifierAttribute` values are usually of type `'string'` (the default), but you can also have values of type `'number'`:\n\n```\n// JS\n\nimport {Component, primaryIdentifier, secondaryIdentifier} from '@layr/component';\n\nclass User extends Component {\n  @primaryIdentifier() id;\n  @secondaryIdentifier('number') reference;\n}\n```\n\n```\n// TS\n\nimport {Component, primaryIdentifier, secondaryIdentifier} from '@layr/component';\n\nclass User extends Component {\n  @primaryIdentifier() id!: string;\n  @secondaryIdentifier('number') reference!: number;\n}\n```\n\n#### Creation\n\n##### `new SecondaryIdentifierAttribute(name, parent, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates an instance of [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute). Typically, instead of using this constructor, you would rather use the [`@secondaryIdentifier()`](https://layrjs.com/docs/v2/reference/component#secondary-identifier-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the attribute.\n* `parent`: The component prototype that owns the attribute.\n* `options`:\n  * `valueType`: A string specifying the type of values the attribute can store. It can be either `'string'` or `'number'` (default: `'string'`).\n  * `default`: A function returning the default value of the attribute.\n  * `validators`: An array of [validators](https://layrjs.com/docs/v2/reference/validator) for the value of the attribute.\n  * `exposure`: A [`PropertyExposure`](https://layrjs.com/docs/v2/reference/property#property-exposure-type) object specifying how the attribute should be exposed to remote access.\n\n**Returns:**\n\nThe [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute) instance that was created.\n\n**Example:**\n\n```\nimport {Component, SecondaryIdentifierAttribute} from '@layr/component';\n\nclass User extends Component {}\n\nconst email = new SecondaryIdentifierAttribute('email', User.prototype);\n\nemail.getName(); // => 'email'\nemail.getParent(); // => User.prototype\nemail.getValueType().toString(); // => 'string'\n```\n\n#### Property Methods\n\nSee the methods that are inherited from the [`Property`](https://layrjs.com/docs/v2/reference/property#basic-methods) class.\n\n#### Attribute Methods\n\nSee the methods that are inherited from the [`Attribute`](https://layrjs.com/docs/v2/reference/attribute#value-type) class.\n\n#### Observability\n\nSee the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) class.\n\n#### Utilities\n\n##### `isSecondaryIdentifierAttributeClass(value)` <badge type=\"tertiary-outline\">function</badge> {#is-secondary-identifier-attribute-class-function}\n\nReturns whether the specified value is a `SecondaryIdentifierAttribute` class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isSecondaryIdentifierAttributeInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-secondary-identifier-attribute-instance-function}\n\nReturns whether the specified value is a `SecondaryIdentifierAttribute` instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/storable-3UMrfeCfFVaVttjxtmCvVy.immutable.md",
    "content": "### Storable() <badge type=\"primary\">mixin</badge> {#storable-mixin}\n\nExtends a [`Component`](https://layrjs.com/docs/v2/reference/component) class with some storage capabilities.\n\n#### Usage\n\nThe `Storable()` mixin can be used both in the backend and the frontend.\n\n##### Backend Usage\n\nCall `Storable()` with a [`Component`](https://layrjs.com/docs/v2/reference/component) class to construct a [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storable-component-class) class that you can extend with your data model and business logic. Then, register this class into a store such as [`MongoDBStore`](https://layrjs.com/docs/v2/reference/mongodb-store) by using the [`registerStorable()`](https://layrjs.com/docs/v2/reference/store#register-storable-instance-method) method (or [`registerRootComponent()`](https://layrjs.com/docs/v2/reference/store#register-root-component-instance-method) to register several components at once).\n\n**Example:**\n\n```js\n// JS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\nimport {MongoDBStore} from '@layr/mongodb-store';\n\nexport class Movie extends Storable(Component) {\n  @primaryIdentifier() id;\n\n  @attribute() title = '';\n}\n\nconst store = new MongoDBStore('mongodb://user:pass@host:port/db');\n\nstore.registerStorable(Movie);\n```\n\n```ts\n// TS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\nimport {MongoDBStore} from '@layr/mongodb-store';\n\nexport class Movie extends Storable(Component) {\n  @primaryIdentifier() id!: string;\n\n  @attribute() title = '';\n}\n\nconst store = new MongoDBStore('mongodb://user:pass@host:port/db');\n\nstore.registerStorable(Movie);\n```\n\nOnce you have a storable component registered into a store, you can use any method provided by the `Storable()` mixin to interact with the database:\n\n```\nconst movie = new Movie({id: 'abc123', title: 'Inception'});\n\n// Save the movie to the database\nawait movie.save();\n\n// Retrieve the movie from the database\nawait Movie.get('abc123'); // => movie\n```\n\n##### Frontend Usage\n\nTypically, you construct a storable component in the frontend by \"inheriting\" a storable component exposed by the backend. To accomplish that, you create a [`ComponentHTTPClient`](https://layrjs.com/docs/v2/reference/component-http-client), and then call the [`getComponent()`](https://layrjs.com/docs/v2/reference/component-http-client#get-component-instance-method) method to construct your frontend component.\n\n**Example:**\n\n```\nimport {ComponentHTTPClient} from '@layr/component-http-client';\nimport {Storable} from '@layr/storable';\n\n(async () => {\n  const client = new ComponentHTTPClient('https://...', {\n    mixins: [Storable]\n  });\n\n  const Movie = await client.getComponent();\n})();\n```\n\n> Note that you have to pass the `Storable` mixin when you create a `ComponentHTTPClient` that is consuming a storable component.\n\nOnce you have a storable component in the frontend, you can use any method that is exposed by the backend. For example, if the `Movie`'s [`save()`](https://layrjs.com/docs/v2/reference/storable#save-instance-method) method is exposed by the backend, you can call it from the frontend to add a new movie into the database:\n\n```\nconst movie = new Movie({title: 'Inception 2'});\n\nawait movie.save();\n```\n\nSee the [\"Storing Data\"](https://layrjs.com/docs/v2/introduction/storing-data) tutorial for a comprehensive example using the `Storable()` mixin.\n\n### StorableComponent <badge type=\"primary\">class</badge> {#storable-component-class}\n\n*Inherits from [`Component`](https://layrjs.com/docs/v2/reference/component).*\n\nA `StorableComponent` class is constructed by calling the `Storable()` mixin ([see above](https://layrjs.com/docs/v2/reference/storable#storable-mixin)).\n\n#### Component Methods\n\nSee the methods that are inherited from the [`Component`](https://layrjs.com/docs/v2/reference/component#creation) class.\n\n#### Store Registration\n\n##### `getStore()` <badge type=\"secondary\">class method</badge> {#get-store-class-method}\n\nReturns the store in which the storable component is registered. If the storable component is not registered in a store, an error is thrown.\n\n**Returns:**\n\nA [`Store`](https://layrjs.com/docs/v2/reference/store) instance.\n\n**Example:**\n\n```\nMovie.getStore(); // => store\n```\n\n##### `hasStore()` <badge type=\"secondary\">class method</badge> {#has-store-class-method}\n\nReturns whether the storable component is registered in a store.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nMovie.hasStore(); // => true\n```\n\n#### Storage Operations\n\n##### `get(identifier, [attributeSelector], [options])` <badge type=\"secondary\">class method</badge> <badge type=\"outline\">async</badge> {#get-class-method}\n\nRetrieves a storable component instance (and possibly, some of its referenced components) from the store.\n\n> This method uses the [`load()`](https://layrjs.com/docs/v2/reference/storable#load-instance-method) method under the hood to load the component's attributes. So if you want to expose the [`get()`](https://layrjs.com/docs/v2/reference/storable#get-class-method) method to the frontend, you will typically have to expose the [`load()`](https://layrjs.com/docs/v2/reference/storable#load-instance-method) method as well.\n\n**Parameters:**\n\n* `identifier`: A plain object specifying the identifier of the component you want to retrieve. The shape of the object should be `{[identifierName]: identifierValue}`. Alternatively, you can specify a string or a number representing the value of a [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute).\n* `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be loaded (default: `true`, which means that all the attributes will be loaded).\n* `options`:\n  * `reload`: A boolean specifying whether a component that has already been loaded should be loaded again from the store (default: `false`). Most of the time you will leave this option off to take advantage of the cache.\n  * `throwIfMissing`: A boolean specifying whether an error should be thrown if there is no component matching the specified `identifier` in the store (default: `true`).\n\n**Returns:**\n\nA [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storable-component-class) instance.\n\n**Example:**\n\n```\n// Fully retrieve a movie by its primary identifier\nawait Movie.get({id: 'abc123'});\n\n// Same as above, but in a short manner\nawait Movie.get('abc123');\n\n// Fully retrieve a movie by its secondary identifier\nawait Movie.get({slug: 'inception'});\n\n// Partially retrieve a movie by its primary identifier\nawait Movie.get({id: 'abc123'}, {title: true, rating: true});\n\n// Partially retrieve a movie, and fully retrieve its referenced director component\nawait Movie.get({id: 'abc123'}, {title: true, director: true});\n\n// Partially retrieve a movie, and partially retrieve its referenced director component\nawait Movie.get({id: 'abc123'}, {title: true, director: {fullName: true}});\n```\n\n##### `has(identifier, [options])` <badge type=\"secondary\">class method</badge> <badge type=\"outline\">async</badge> {#has-class-method}\n\nReturns whether a storable component instance exists in the store.\n\n**Parameters:**\n\n* `identifier`: A plain object specifying the identifier of the component you want to search. The shape of the object should be `{[identifierName]: identifierValue}`. Alternatively, you can specify a string or a number representing the value of a [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute).\n* `options`:\n  * `reload`: A boolean specifying whether a component that has already been loaded should be searched again from the store (default: `false`). Most of the time you will leave this option off to take advantage of the cache.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\n// Check if there is a movie with a certain primary identifier\nawait Movie.has({id: 'abc123'}); // => true\n\n// Same as above, but in a short manner\nawait Movie.has('abc123'); // => true\n\n// Check if there is a movie with a certain secondary identifier\nawait Movie.has({slug: 'inception'}); // => true\n```\n\n##### `load([attributeSelector], [options])` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#load-instance-method}\n\nLoads some attributes of the current storable component instance (and possibly, some of its referenced components) from the store.\n\n**Parameters:**\n\n* `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be loaded (default: `true`, which means that all the attributes will be loaded).\n* `options`:\n  * `reload`: A boolean specifying whether a component that has already been loaded should be loaded again from the store (default: `false`). Most of the time you will leave this option off to take advantage of the cache.\n  * `throwIfMissing`: A boolean specifying whether an error should be thrown if there is no matching component in the store (default: `true`).\n\n**Returns:**\n\nThe current [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storable-component-class) instance.\n\n**Example:**\n\n```\n// Retrieve a movie with the 'title' attribute only\nconst movie = await Movie.get('abc123', {title: true});\n\n// Load a few more movie's attributes\nawait movie.load({tags: true, rating: true});\n\n// Load some attributes of the movie's director\nawait movie.load({director: {fullName: true}});\n\n// Since the movie's rating has already been loaded,\n// it will not be loaded again from the store\nawait movie.load({rating: true});\n\n// Change the movie's rating\nmovie.rating = 8.5;\n\n// Since the movie's rating has been modified,\n// it will be loaded again from the store\nawait movie.load({rating: true});\n\n// Force reloading the movie's rating\nawait movie.load({rating: true}, {reload: true});\n```\n\n##### `save([attributeSelector], [options])` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#save-instance-method}\n\nSaves the current storable component instance to the store. If the component is new, it will be added to the store with all its attributes. Otherwise, only the attributes that have been modified will be saved to the store.\n\n**Parameters:**\n\n* `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be saved (default: `true`, which means that all the modified attributes will be saved).\n* `options`:\n  * `throwIfMissing`: A boolean specifying whether an error should be thrown if the current component is not new and there is no existing component with the same identifier in the store (default: `true` if the component is not new).\n  * `throwIfExists`: A boolean specifying whether an error should be thrown if the current component is new and there is an existing component with the same identifier in the store (default: `true` if the component is new).\n\n**Returns:**\n\nThe current [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storable-component-class) instance.\n\n**Example:**\n\n```\n// Retrieve a movie with a few attributes\nconst movie = await Movie.get('abc123', {title: true, rating: true});\n\n// Change the movie's rating\nmovie.rating = 8;\n\n// Save the new movie's rating to the store\nawait movie.save();\n\n// Since the movie's rating has not been changed since the previous save(),\n// it will not be saved again\nawait movie.save();\n```\n\n##### `delete([options])` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#delete-instance-method}\n\nDeletes the current storable component instance from the store.\n\n**Parameters:**\n\n* `options`:\n  * `throwIfMissing`: A boolean specifying whether an error should be thrown if there is no matching component in the store (default: `true`).\n\n**Returns:**\n\nThe current [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storable-component-class) instance.\n\n**Example:**\n\n```\n// Retrieve a movie\nconst movie = await Movie.get('abc123');\n\n// Delete the movie\nawait movie.delete();\n```\n\n##### `find([query], [attributeSelector], [options])` <badge type=\"secondary\">class method</badge> <badge type=\"outline\">async</badge> {#find-class-method}\n\nFinds some storable component instances matching the specified query in the store, and load all or some of their attributes (and possibly, load some of their referenced components as well).\n\n> This method uses the [`load()`](https://layrjs.com/docs/v2/reference/storable#load-instance-method) method under the hood to load the components' attributes. So if you want to expose the [`find()`](https://layrjs.com/docs/v2/reference/storable#find-class-method) method to the frontend, you will typically have to expose the [`load()`](https://layrjs.com/docs/v2/reference/storable#load-instance-method) method as well.\n\n**Parameters:**\n\n* `query`: A [`Query`](https://layrjs.com/docs/v2/reference/query) object specifying the criteria to be used when selecting the components from the store (default: `{}`, which means that any component can be selected).\n* `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) specifying the attributes to be loaded (default: `true`, which means that all the attributes will be loaded).\n* `options`:\n  * `sort`: A plain object specifying how the found components should be sorted (default: `undefined`). The shape of the object should be `{[name]: direction}` where `name` is the name of an attribute, and `direction` is the string `'asc'` or `'desc'` representing the sort direction (ascending or descending).\n  * `skip`: A number specifying how many components should be skipped from the found components (default: `0`).\n  * `limit`: A number specifying the maximum number of components that should be returned (default: `undefined`).\n  * `reload`: A boolean specifying whether a component that has already been loaded should be loaded again from the store (default: `false`). Most of the time you will leave this option off to take advantage of the cache.\n\n**Returns:**\n\nAn array of [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storable-component-class) instances.\n\n**Example:**\n\n```\n// Find all the movies\nawait Movie.find();\n\n// Find the Japanese movies\nawait Movie.find({country: 'Japan'});\n\n// Find the Japanese drama movies\nawait Movie.find({country: 'Japan', genre: 'drama'});\n\n// Find the Tarantino's movies\nconst tarantino = await Director.get({slug: 'quentin-tarantino'});\nawait Movie.find({director: tarantino});\n\n// Find the movies released after 2010\nawait Movie.find({year: {$greaterThan: 2010}});\n\n// Find the top 30 movies\nawait Movie.find({}, true, {sort: {rating: 'desc'}, limit: 30});\n\n// Find the next top 30 movies\nawait Movie.find({}, true, {sort: {rating: 'desc'}, skip: 30, limit: 30});\n```\n\n##### `count([query])` <badge type=\"secondary\">class method</badge> <badge type=\"outline\">async</badge> {#count-class-method}\n\nCounts the number of storable component instances matching the specified query in the store.\n\n**Parameters:**\n\n* `query`: A [`Query`](https://layrjs.com/docs/v2/reference/query) object specifying the criteria to be used when selecting the components from the store (default: `{}`, which means that any component can be selected, and therefore the total number of components available in the store will be returned).\n\n**Returns:**\n\nA number.\n\n**Example:**\n\n```\n// Count the total number of movies\nawait Movie.count();\n\n// Count the number of Japanese movies\nawait Movie.count({country: 'Japan'});\n\n// Count the number of Japanese drama movies\nawait Movie.count({country: 'Japan', genre: 'drama'});\n\n// Count the number of Tarantino's movies\nconst tarantino = await Director.get({slug: 'quentin-tarantino'})\nawait Movie.count({director: tarantino});\n\n// Count the number of movies released after 2010\nawait Movie.count({year: {$greaterThan: 2010}});\n```\n\n#### isDeleted Mark\n\n##### `getIsDeletedMark()` <badge type=\"secondary-outline\">instance method</badge> {#get-is-deleted-mark-instance-method}\n\nReturns whether the component instance is marked as deleted or not.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nmovie.getIsDeletedMark(); // => false\nawait movie.delete();\nmovie.getIsDeletedMark(); // => true\n```\n\n##### `setIsDeletedMark(isDeleted)` <badge type=\"secondary-outline\">instance method</badge> {#set-is-deleted-mark-instance-method}\n\nSets whether the component instance is marked as deleted or not.\n\n**Parameters:**\n\n* `isDeleted`: A boolean specifying if the component instance should be marked as deleted or not.\n\n**Example:**\n\n```\nmovie.getIsDeletedMark(); // => false\nmovie.setIsDeletedMark(true);\nmovie.getIsDeletedMark(); // => true\n```\n\n#### Hooks\n\n##### `beforeLoad(attributeSelector)` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#before-load-instance-method}\n\nA method that you can override to execute some custom logic just before the current storable component instance is loaded from the store.\n\nThis method is automatically called when the [`load()`](https://layrjs.com/docs/v2/reference/storable#load-instance-method), [`get()`](https://layrjs.com/docs/v2/reference/storable#get-class-method), or [`find()`](https://layrjs.com/docs/v2/reference/storable#find-class-method) method is called, and there are some attributes to load. If all the attributes have already been loaded by a previous operation, unless the `reload` option is used, this method is not called.\n\n**Parameters:**\n\n* `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) indicating the attributes that will be loaded.\n\n**Example:**\n\n```\n// JS\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  async beforeLoad(attributeSelector) {\n    // Don't forget to call the parent method\n    await super.beforeLoad(attributeSelector);\n\n    // Implement your custom logic here\n  }\n}\n```\n```\n// TS\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  async beforeLoad(attributeSelector: AttributeSelector) {\n    // Don't forget to call the parent method\n    await super.beforeLoad(attributeSelector);\n\n    // Implement your custom logic here\n  }\n}\n```\n\n##### `afterLoad(attributeSelector)` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#after-load-instance-method}\n\nA method that you can override to execute some custom logic just after the current storable component instance has been loaded from the store.\n\nThis method is automatically called when the [`load()`](https://layrjs.com/docs/v2/reference/storable#load-instance-method), [`get()`](https://layrjs.com/docs/v2/reference/storable#get-class-method), or [`find()`](https://layrjs.com/docs/v2/reference/storable#find-class-method) method is called, and there were some attributes to load. If all the attributes have already been loaded by a previous operation, unless the `reload` option is used, this method is not called.\n\n**Parameters:**\n\n* `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) indicating the attributes that were loaded.\n\n**Example:**\n\n```\n// JS\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  async afterLoad(attributeSelector) {\n    // Don't forget to call the parent method\n    await super.afterLoad(attributeSelector);\n\n    // Implement your custom logic here\n  }\n}\n```\n```\n// TS\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  async afterLoad(attributeSelector: AttributeSelector) {\n    // Don't forget to call the parent method\n    await super.afterLoad(attributeSelector);\n\n    // Implement your custom logic here\n  }\n}\n```\n\n##### `beforeSave(attributeSelector)` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#before-save-instance-method}\n\nA method that you can override to execute some custom logic just before the current storable component instance is saved to the store.\n\nThis method is automatically called when the [`save()`](https://layrjs.com/docs/v2/reference/storable#save-instance-method) method is called, and there are some modified attributes to save. If no attributes were modified, this method is not called.\n\n**Parameters:**\n\n* `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) indicating the attributes that will be saved.\n\n**Example:**\n\n```\n// JS\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  async beforeSave(attributeSelector) {\n    // Don't forget to call the parent method\n    await super.beforeSave(attributeSelector);\n\n    // Implement your custom logic here\n  }\n}\n```\n```\n// TS\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  async beforeSave(attributeSelector: AttributeSelector) {\n    // Don't forget to call the parent method\n    await super.beforeSave(attributeSelector);\n\n    // Implement your custom logic here\n  }\n}\n```\n\n##### `afterSave(attributeSelector)` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#after-save-instance-method}\n\nA method that you can override to execute some custom logic just after the current storable component instance has been saved to the store.\n\nThis method is automatically called when the [`save()`](https://layrjs.com/docs/v2/reference/storable#save-instance-method) method is called, and there were some modified attributes to save. If no attributes were modified, this method is not called.\n\n**Parameters:**\n\n* `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) indicating the attributes that were saved.\n\n**Example:**\n\n```\n// JS\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  async afterSave(attributeSelector) {\n    // Don't forget to call the parent method\n    await super.afterSave(attributeSelector);\n\n    // Implement your custom logic here\n  }\n}\n```\n```\n// TS\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  async afterSave(attributeSelector: AttributeSelector) {\n    // Don't forget to call the parent method\n    await super.afterSave(attributeSelector);\n\n    // Implement your custom logic here\n  }\n}\n```\n\n##### `beforeDelete(attributeSelector)` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#before-delete-instance-method}\n\nA method that you can override to execute some custom logic just before the current storable component instance is deleted from the store.\n\nThis method is automatically called when the [`delete()`](https://layrjs.com/docs/v2/reference/storable#delete-instance-method) method is called.\n\n**Parameters:**\n\n* `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) indicating the attributes that will be deleted.\n\n**Example:**\n\n```\n// JS\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  async beforeDelete(attributeSelector) {\n    // Don't forget to call the parent method\n    await super.beforeDelete(attributeSelector);\n\n    // Implement your custom logic here\n  }\n}\n```\n```\n// TS\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  async beforeDelete(attributeSelector: AttributeSelector) {\n    // Don't forget to call the parent method\n    await super.beforeDelete(attributeSelector);\n\n    // Implement your custom logic here\n  }\n}\n```\n\n##### `afterDelete(attributeSelector)` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#after-delete-instance-method}\n\nA method that you can override to execute some custom logic just after the current storable component instance has been deleted from the store.\n\nThis method is automatically called when the [`delete()`](https://layrjs.com/docs/v2/reference/storable#delete-instance-method) method is called.\n\n**Parameters:**\n\n* `attributeSelector`: An [`AttributeSelector`](https://layrjs.com/docs/v2/reference/attribute-selector) indicating the attributes that were deleted.\n\n**Example:**\n\n```\n// JS\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  async afterDelete(attributeSelector) {\n    // Don't forget to call the parent method\n    await super.afterDelete(attributeSelector);\n\n    // Implement your custom logic here\n  }\n}\n```\n```\n// TS\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  async afterDelete(attributeSelector: AttributeSelector) {\n    // Don't forget to call the parent method\n    await super.afterDelete(attributeSelector);\n\n    // Implement your custom logic here\n  }\n}\n```\n\n#### Observability\n\nSee the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) class.\n\n#### Decorators\n\n##### `@attribute([valueType], [options])` <badge type=\"tertiary\">decorator</badge> {#attribute-decorator}\n\nDecorates an attribute of a storable component so it can be combined with a [`Loader`](https://layrjs.com/docs/v2/reference/storable-attribute#loader-type), a [`Finder`](https://layrjs.com/docs/v2/reference/storable-property#finder-type), or any kind of [`Hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type).\n\n**Parameters:**\n\n* `valueType`: A string specifying the [type of values](https://layrjs.com/docs/v2/reference/value-type#supported-types) that can be stored in the attribute (default: `'any'`).\n* `options`: The options to create the [`StorableAttribute`](https://layrjs.com/docs/v2/reference/storable-attribute#constructor).\n\n**Example:**\n\nSee an example of use in the [`StorableAttribute`](https://layrjs.com/docs/v2/reference/storable-attribute) class.\n##### `@primaryIdentifier([valueType], [options])` <badge type=\"tertiary\">decorator</badge> {#primary-identifier-decorator}\n\nDecorates an attribute of a component as a [storable primary identifier attribute](https://layrjs.com/docs/v2/reference/storable-primary-identifier-attribute).\n\n**Parameters:**\n\n* `valueType`: A string specifying the type of values the attribute can store. It can be either `'string'` or `'number'` (default: `'string'`).\n* `options`: The options to create the [`StorablePrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/storable-primary-identifier-attribute#constructor).\n\n##### `@secondaryIdentifier([valueType], [options])` <badge type=\"tertiary\">decorator</badge> {#secondary-identifier-decorator}\n\nDecorates an attribute of a component as a [storable secondary identifier attribute](https://layrjs.com/docs/v2/reference/storable-secondary-identifier-attribute).\n\n**Parameters:**\n\n* `valueType`: A string specifying the type of values the attribute can store. It can be either `'string'` or `'number'` (default: `'string'`).\n* `options`: The options to create the [`StorableSecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/storable-secondary-identifier-attribute#constructor).\n\n##### `@method([options])` <badge type=\"tertiary\">decorator</badge> {#method-decorator}\n\nDecorates a method of a storable component so it can be combined with a [`Finder`](https://layrjs.com/docs/v2/reference/storable-property#finder-type).\n\n**Parameters:**\n\n* `options`: The options to create the [`StorableMethod`](https://layrjs.com/docs/v2/reference/storable-method#constructor).\n\n**Example:**\n\nSee an example of use in the [`StorableMethod`](https://layrjs.com/docs/v2/reference/storable-method) class.\n##### `@loader(loader)` <badge type=\"tertiary\">decorator</badge> {#loader-decorator}\n\nDecorates a storable attribute with a [`Loader`](https://layrjs.com/docs/v2/reference/storable-attribute#loader-type).\n\n**Parameters:**\n\n* `loader`: A function representing the [`Loader`](https://layrjs.com/docs/v2/reference/storable-attribute#loader-type) of the storable attribute.\n\n**Example:**\n\nSee an example of use in the [`StorableAttribute`](https://layrjs.com/docs/v2/reference/storable-attribute) class.\n##### `@finder(finder)` <badge type=\"tertiary\">decorator</badge> {#finder-decorator}\n\nDecorates a storable attribute or method with a [`Finder`](https://layrjs.com/docs/v2/reference/storable-property#finder-type).\n\n**Parameters:**\n\n* `finder`: A function representing the [`Finder`](https://layrjs.com/docs/v2/reference/storable-property#finder-type) of the storable attribute or method.\n\n**Example:**\n\nSee an example of use in the [`StorableAttribute`](https://layrjs.com/docs/v2/reference/storable-attribute) and [`StorableMethod`](https://layrjs.com/docs/v2/reference/storable-method) classes.\n##### `@index([optionsOrAttributes], [options])` <badge type=\"tertiary\">decorator</badge> {#index-decorator}\n\nDefines an [index](https://layrjs.com/docs/v2/reference/index) for an attribute or a set of attributes.\n\nThis decorator is commonly placed before a storable component attribute to define a [single attribute index](https://layrjs.com/docs/v2/reference/index#single-attribute-indexes), but it can also be placed before a storable component class to define a [compound attribute index](https://layrjs.com/docs/v2/reference/index#compound-attribute-indexes).\n\n**Parameters:**\n\n* `optionsOrAttributes`: Depends on the type of index you want to define (see below).\n* `options`: An object specifying some options in the case of compound attribute index (see below).\n\n###### Single Attribute Indexes\n\nYou can define an index for a single attribute by placing the `@index()` decorator before an attribute definition. In this case, you can specify the following parameters:\n\n- `options`:\n  - `direction`: A string representing the sort direction of the index. The possible values are `'asc'` (ascending) or `'desc'` (descending) and the default value is `'asc'`.\n  - `isUnique`: A boolean specifying whether the index should hold unique values or not (default: `false`). When set to `true`, the underlying database will prevent you to store an attribute with the same value in multiple storable components.\n\n###### Compound Attribute Indexes\n\nYou can define an index that combines multiple attributes by placing the `@index()` decorator before a storable component class definition. In this case, you can specify the following parameters:\n\n- `attributes`: An object specifying the attributes to be indexed. The shape of the object should be `{attributeName: direction, ...}` where `attributeName` is a string representing the name of an attribute and `direction` is a string representing the sort direction (possible values: `'asc'` or `'desc'`).\n- `options`:\n  - `isUnique`: A boolean specifying whether the index should hold unique values or not (default: `false`). When set to `true`, the underlying database will prevent you to store an attribute with the same value in multiple storable components.\n\n**Example:**\n\n```\n// JS\n\nimport {Component} from '@layr/component';\nimport {Storable, attribute, index} from '@layr/storable';\n\n// An index that combines the `year` and `title` attributes:\n@index({year: 'desc', title: 'asc'})\nexport class Movie extends Storable(Component) {\n  // An index for the `title` attribute with the `isUnique` option:\n  @index({isUnique: true}) @attribute('string') title;\n\n  // An index for the `year` attribute with the `'desc'` sort direction:\n  @index({direction: 'desc'}) @attribute('number') year;\n}\n```\n```\n// TS\n\nimport {Component} from '@layr/component';\nimport {Storable, attribute, index} from '@layr/storable';\n\n// An index that combines the `year` and `title` attributes:\n@index({year: 'desc', title: 'asc'})\nexport class Movie extends Storable(Component) {\n  // An index for the `title` attribute with the `isUnique` option:\n  @index({isUnique: true}) @attribute('string') title!: string;\n\n  // An index for the `year` attribute with the `'desc'` sort direction:\n  @index({direction: 'desc'}) @attribute('number') year!: string;\n}\n```\n\n#### Utilities\n\n##### `isStorableClass(value)` <badge type=\"tertiary-outline\">function</badge> {#is-storable-class-function}\n\nReturns whether the specified value is a storable component class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isStorableInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-storable-instance-function}\n\nReturns whether the specified value is a storable component instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isStorableClassOrInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-storable-class-or-instance-function}\n\nReturns whether the specified value is a storable component class or instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `assertIsStorableClass(value)` <badge type=\"tertiary-outline\">function</badge> {#assert-is-storable-class-function}\n\nThrows an error if the specified value is not a storable component class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n##### `assertIsStorableInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#assert-is-storable-instance-function}\n\nThrows an error if the specified value is not a storable component instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n##### `assertIsStorableClassOrInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#assert-is-storable-class-or-instance-function}\n\nThrows an error if the specified value is not a storable component class or instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n##### `ensureStorableClass(component)` <badge type=\"tertiary-outline\">function</badge> {#ensure-storable-class-function}\n\nEnsures that the specified storable component is a class. If you specify a storable component instance (or prototype), the class of the component is returned. If you specify a storable component class, it is returned as is.\n\n**Parameters:**\n\n* `component`: A storable component class or instance.\n\n**Returns:**\n\nA storable component class.\n\n**Example:**\n\n```\nensureStorableClass(movie) => Movie\nensureStorableClass(Movie.prototype) => Movie\nensureStorableClass(Movie) => Movie\n```\n\n##### `ensureStorableInstance(component)` <badge type=\"tertiary-outline\">function</badge> {#ensure-storable-instance-function}\n\nEnsures that the specified storable component is an instance (or prototype). If you specify a storable component class, the component prototype is returned. If you specify a storable component instance (or prototype), it is returned as is.\n\n**Parameters:**\n\n* `component`: A storable component class or instance.\n\n**Returns:**\n\nA storable component instance (or prototype).\n\n**Example:**\n\n```\nensureStorableInstance(Movie) => Movie.prototype\nensureStorableInstance(Movie.prototype) => Movie.prototype\nensureStorableInstance(movie) => movie\n```\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/storable-attribute-1EYIvaUK2WvHmV3OxG5IzJ.immutable.md",
    "content": "### StorableAttribute <badge type=\"primary\">class</badge> {#storable-attribute-class}\n\n*Inherits from [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) and [`StorableProperty`](https://layrjs.com/docs/v2/reference/storable-property).*\n\nThe `StorableAttribute` class extends the [`Attribute`](https://layrjs.com/docs/v2/reference/attribute) class with some capabilities such as [computed attributes](https://layrjs.com/docs/v2/reference/storable-attribute#computed-attributes) or [hooks](https://layrjs.com/docs/v2/reference/storable-attribute#hooks).\n\n#### Usage\n\nTypically, you create a `StorableAttribute` and associate it to a [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class) using the [`@attribute()`](https://layrjs.com/docs/v2/reference/storable#attribute-decorator) decorator.\n\nFor example, here is how you would define a `Movie` component with some attributes:\n\n```\n// JS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  @primaryIdentifier() id;\n\n  @attribute('string') title = '';\n\n  @attribute('number') rating;\n\n  @attribute('Date') releaseDate;\n}\n```\n\n```\n// TS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  @primaryIdentifier() id!: string;\n\n  @attribute('string') title = '';\n\n  @attribute('number') rating!: number;\n\n  @attribute('Date') releaseDate!: Date;\n}\n```\n\nSo far we've defined some storable attributes in the same way we would do with [regular attributes](https://layrjs.com/docs/v2/reference/attribute). The only difference is that we imported the [`@attribute()`](https://layrjs.com/docs/v2/reference/storable#attribute-decorator) decorator from `@layr/storable` instead of `@layr/component`.\n\nLet's now see how to take advantage of some capabilities that are unique to storable attributes.\n\n##### Computed Attributes\n\nA computed attribute is a special kind of component attribute that computes its value when the component is loaded with a storable component method such as [`load()`](https://layrjs.com/docs/v2/reference/storable#load-instance-method), [`get()`](https://layrjs.com/docs/v2/reference/storable#get-class-method), or [`find()`](https://layrjs.com/docs/v2/reference/storable#find-class-method).\n\nThe value of a computed attribute shouldn't be set manually, and is not persisted when you [save](https://layrjs.com/docs/v2/reference/storable#save-instance-method) a component to a store.\n\n###### Loaders\n\nUse the [`@loader()`](https://layrjs.com/docs/v2/reference/storable#loader-decorator) decorator to specify the function that computes the value of a computed attribute.\n\nFor example, let's define a `isTrending` computed attribute that determines its value according to the movie's `rating` and `releaseDate`:\n\n```\n// JS\n\nimport {loader} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  @loader(async function() {\n    await this.load({rating: true, releaseDate: true});\n\n    const ratingLimit = 7;\n    const releaseDateLimit = new Date(Date.now() - 864000000); // 10 days before\n\n    return this.rating >= ratingLimit && this.releaseDate >= releaseDateLimit;\n  })\n  @attribute('boolean')\n  isTrending;\n}\n```\n\n```\n// TS\n\nimport {loader} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  @loader(async function(this: Movie) {\n    await this.load({rating: true, releaseDate: true});\n\n    const ratingLimit = 7;\n    const releaseDateLimit = new Date(Date.now() - 864000000); // 10 days before\n\n    return this.rating >= ratingLimit && this.releaseDate >= releaseDateLimit;\n  })\n  @attribute('boolean')\n  isTrending!: boolean;\n}\n```\n\nThen, when we get a movie, we can get the `isTrending` computed attribute like any attribute:\n\n```\nconst movie = await Movie.get('abc123', {title: true, isTrending: true});\n\nmovie.title; // => 'Inception'\nmovie.isTrending; // => true (on July 16th, 2010)\n```\n\n###### Finders\n\nThe best thing about computed attributes is that they can be used in a [`Query`](https://layrjs.com/docs/v2/reference/query) when you are [finding](https://layrjs.com/docs/v2/reference/storable#find-class-method) or [counting](https://layrjs.com/docs/v2/reference/storable#count-class-method) some storable components.\n\nTo enable that, use the [`@finder()`](https://layrjs.com/docs/v2/reference/storable#finder-decorator) decorator, and specify a function that returns a [`Query`](https://layrjs.com/docs/v2/reference/query).\n\nFor example, let's make our `isTrending` attribute searchable by adding a finder:\n\n```\n// JS\n\nimport {loader} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  @loader(\n    // ...\n  )\n  @finder(function (isTrending) {\n    const ratingLimit = 7;\n    const releaseDateLimit = new Date(Date.now() - 864000000); // 10 days before\n\n    if (isTrending) {\n      // Return a query for `{isTrending: true}`\n      return {\n        rating: {$greaterThanOrEqual: ratingLimit}\n        releaseDate: {$greaterThanOrEqual: releaseDateLimit}\n      };\n    }\n\n    // Return a query for `{isTrending: false}`\n    return {\n      $or: [\n        {rating: {$lessThan: ratingLimit}},\n        {releaseDate: {$lessThan: releaseDateLimit}}\n      ]\n    };\n  })\n  @attribute('boolean')\n  isTrending;\n}\n```\n\n```\n// TS\n\nimport {loader} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  @loader(\n    // ...\n  )\n  @finder(function (isTrending: boolean) {\n    const ratingLimit = 7;\n    const releaseDateLimit = new Date(Date.now() - 864000000); // 10 days before\n\n    if (isTrending) {\n      // Return a query for `{isTrending: true}`\n      return {\n        rating: {$greaterThanOrEqual: ratingLimit}\n        releaseDate: {$greaterThanOrEqual: releaseDateLimit}\n      };\n    }\n\n    // Return a query for `{isTrending: false}`\n    return {\n      $or: [\n        {rating: {$lessThan: ratingLimit}},\n        {releaseDate: {$lessThan: releaseDateLimit}}\n      ]\n    };\n  })\n  @attribute('boolean')\n  isTrending!: boolean;\n}\n```\n\nAnd now, we can query our `isTrending` computed attribute like we would do with any attribute:\n\n```\nawait Movie.find({isTrending: true}); // => All trending movies\nawait Movie.find({isTrending: false}); // => All non-trending movies\n\nawait Movie.count({isTrending: true}); // => Number of trending movies\nawait Movie.count({isTrending: false}); // => Number of non-trending movies\n\n// Combine computed attributes with regular attributes to find\n// all Japanese trending movies\nawait Movie.find({country: 'Japan', isTrending: true});\n```\n\n##### Hooks\n\nStorable attributes offer a number of hooks that you can use to execute some custom logic when an attribute is loaded, saved or deleted.\n\nTo define a hook for a storable attribute, use one of the following [`@attribute()`](https://layrjs.com/docs/v2/reference/storable#attribute-decorator) options:\n\n- `beforeLoad`: Specifies a [`Hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) to be executed *before* an attribute is *loaded*.\n- `afterLoad`: Specifies a [`Hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) to be executed *after* an attribute is *loaded*.\n- `beforeSave`: Specifies a [`Hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) to be executed *before* an attribute is *saved*.\n- `afterSave`: Specifies a [`Hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) to be executed *after* an attribute is *saved*.\n- `beforeDelete`: Specifies a [`Hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) to be executed *before* an attribute is *deleted*.\n- `afterDelete`: Specifies a [`Hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) to be executed *after* an attribute is *deleted*.\n\nFor example, we could use a `beforeSave` hook to make sure a user's password is hashed before it is saved to a store:\n\n```\n// JS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\nimport bcrypt from 'bcryptjs';\n\nclass User extends Storable(Component) {\n  @primaryIdentifier() id;\n\n  @attribute('string') username;\n\n  @attribute('string', {\n    async beforeSave() {\n      this.password = await bcrypt.hash(this.password);\n    }\n  })\n  password;\n}\n```\n\n```\n// TS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\nimport bcrypt from 'bcryptjs';\n\nclass User extends Storable(Component) {\n  @primaryIdentifier() id!: string;\n\n  @attribute('string') username!: string;\n\n  @attribute('string', {\n    async beforeSave(this: User) {\n      this.password = await bcrypt.hash(this.password);\n    }\n  })\n  password!: string;\n}\n```\n\nThen, when we save a user, its password gets automatically hashed:\n```\nconst user = new User({username: 'steve', password: 'zyx98765'});\n\nuser.password; // => 'zyx98765'\n\nawait user.save(); // The password will be hashed before saved to the store\n\nuser.password; // => '$2y$12$AGJ91pnqlM7TcqnLg0iIFuiN80z9k.wFnGVl1a4lrANUepBKmvNVO'\n\n// Note that if we save the user again, as long as its password hasn't changed,\n// it will not be saved, and therefore not be hashed again\n\nuser.username = 'steve2';\nawait user.save(); // Only the username will be saved\n\nuser.password; // => '$2y$12$AGJ91pnqlM7TcqnLg0iIFuiN80z9k.wFnGVl1a4lrANUepBKmvNVO'\n```\n\n#### Creation\n\n##### `new StorableAttribute(name, parent, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates a storable attribute. Typically, instead of using this constructor, you would rather use the [`@attribute()`](https://layrjs.com/docs/v2/reference/storable#attribute-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the attribute.\n* `parent`: The [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class) class, prototype, or instance that owns the attribute.\n* `options`:\n  * `valueType`: A string specifying the [type of values](https://layrjs.com/docs/v2/reference/value-type#supported-types) the attribute can store (default: `'any'`).\n  * `default`: The default value (or a function returning the default value) of the attribute.\n  * `validators`: An array of [validators](https://layrjs.com/docs/v2/reference/validator) for the value of the attribute.\n  * `items`:\n    * `validators`: An array of [validators](https://layrjs.com/docs/v2/reference/validator) for the items of an array attribute.\n  * `loader`: A function specifying a [`Loader`](https://layrjs.com/docs/v2/reference/storable-attribute#loader-type) for the attribute.\n  * `finder`: A function specifying a [`Finder`](https://layrjs.com/docs/v2/reference/storable-property#finder-type) for the attribute.\n  * `beforeLoad`: A function specifying a \"beforeLoad\" [`hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) for the attribute.\n  * `afterLoad`: A function specifying an \"afterLoad\" [`hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) for the attribute.\n  * `beforeSave`: A function specifying a \"beforeSave\" [`hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) for the attribute.\n  * `afterSave`: A function specifying an \"afterSave\" [`hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) for the attribute.\n  * `beforeDelete`: A function specifying a \"beforeDelete\" [`hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) for the attribute.\n  * `afterDelete`: A function specifying an \"afterDelete\" [`hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) for the attribute.\n  * `exposure`: A [`PropertyExposure`](https://layrjs.com/docs/v2/reference/property#property-exposure-type) object specifying how the attribute should be exposed to remote access.\n\n**Returns:**\n\nThe [`StorableAttribute`](https://layrjs.com/docs/v2/reference/storable-attribute) instance that was created.\n\n#### Property Methods\n\nSee the methods that are inherited from the [`Property`](https://layrjs.com/docs/v2/reference/property#basic-methods) class.\n\n#### Attribute Methods\n\nSee the methods that are inherited from the [`Attribute`](https://layrjs.com/docs/v2/reference/attribute#value-type) class.\n\n#### Loader\n\n##### `getLoader()` <badge type=\"secondary-outline\">instance method</badge> {#get-loader-instance-method}\n\nReturns the [`Loader`](https://layrjs.com/docs/v2/reference/storable-attribute#loader-type) of the attribute.\n\n**Returns:**\n\nA [`Loader`](https://layrjs.com/docs/v2/reference/storable-attribute#loader-type) function (or `undefined` if the attribute has no associated loader).\n\n##### `hasLoader()` <badge type=\"secondary-outline\">instance method</badge> {#has-loader-instance-method}\n\nReturns whether the attribute has a [`Loader`](https://layrjs.com/docs/v2/reference/storable-attribute#loader-type).\n\n**Returns:**\n\nA boolean.\n\n##### `setLoader(loader)` <badge type=\"secondary-outline\">instance method</badge> {#set-loader-instance-method}\n\nSets a [`Loader`](https://layrjs.com/docs/v2/reference/storable-attribute#loader-type) for the attribute.\n\n**Parameters:**\n\n* `loader`: The [`Loader`](https://layrjs.com/docs/v2/reference/storable-attribute#loader-type) function to set.\n\n##### `Loader` <badge type=\"primary-outline\">type</badge> {#loader-type}\n\nA function representing the \"loader\" of an attribute.\n\nThe function should return a value for the attribute that is being loaded. Typically, you would return a value according to the value of some other attributes.\n\nThe function can be `async` and is executed with the parent of the attribute as `this` context.\n\nSee an example of use in the [\"Computed Attributes\"](https://layrjs.com/docs/v2/reference/storable-attribute#computed-attributes) section above.\n\n#### Finder\n\nSee the methods that are inherited from the [`StorableProperty`](https://layrjs.com/docs/v2/reference/storable-property#finder) class.\n\n#### Hooks\n\n##### `getHook(name)` <badge type=\"secondary-outline\">instance method</badge> {#get-hook-instance-method}\n\nReturns a specific [`Hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) for the current attribute.\n\n**Parameters:**\n\n* `name`: A string representing the name of the hook you want to get. The possible values are `'beforeLoad'`, `'afterLoad'`, `'beforeSave'`, `'afterSave'`, `'beforeDelete'`, and `'afterDelete'`.\n\n**Returns:**\n\nA [`Hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) function (or `undefined` if the attribute doesn't have a hook with the specified `name`).\n\n##### `hasHook(name)` <badge type=\"secondary-outline\">instance method</badge> {#has-hook-instance-method}\n\nReturns whether the current attribute has a specific [`Hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type).\n\n**Parameters:**\n\n* `name`: A string representing the name of the hook you want to check. The possible values are `'beforeLoad'`, `'afterLoad'`, `'beforeSave'`, `'afterSave'`, `'beforeDelete'`, and `'afterDelete'`.\n\n**Returns:**\n\nA boolean.\n\n##### `setHook(name, hook)` <badge type=\"secondary-outline\">instance method</badge> {#set-hook-instance-method}\n\nSets a [`Hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) for the current attribute.\n\n**Parameters:**\n\n* `name`: A string representing the name of the hook you want to set. The possible values are `'beforeLoad'`, `'afterLoad'`, `'beforeSave'`, `'afterSave'`, `'beforeDelete'`, and `'afterDelete'`.\n* `hook`: The [`Hook`](https://layrjs.com/docs/v2/reference/storable-attribute#hook-type) function to set.\n\n##### `Hook` <badge type=\"primary-outline\">type</badge> {#hook-type}\n\nA function representing a \"hook\" of an attribute.\n\nAccording to the type of the hook, the function is automatically called when an attribute is loaded, saved or deleted.\n\nThe function can be `async` and is invoked with the attribute as first parameter and the parent of the attribute (i.e., the storable component) as `this` context.\n\nSee an example of use in the [\"Hooks\"](https://layrjs.com/docs/v2/reference/storable-attribute#hooks) section above.\n\n#### Observability\n\nSee the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) class.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/storable-method-1o1egWUfiZzxO6y7Sda4rb.immutable.md",
    "content": "### StorableMethod <badge type=\"primary\">class</badge> {#storable-method-class}\n\n*Inherits from [`Method`](https://layrjs.com/docs/v2/reference/method) and [`StorableProperty`](https://layrjs.com/docs/v2/reference/storable-property).*\n\nThe `StorableMethod` class extends the [`Method`](https://layrjs.com/docs/v2/reference/method) class with the capabilities of the [`StorableProperty`](https://layrjs.com/docs/v2/reference/storable-property) class.\n\nIn a nutshell, using the `StorableMethod` class allows you to associate a [`Finder`](https://layrjs.com/docs/v2/reference/storable-property#finder-type) to a method so this method can be used in a [`Query`](https://layrjs.com/docs/v2/reference/query).\n\n\n#### Usage\n\nTypically, you create a `StorableMethod` and associate it to a [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class) using the [`@method()`](https://layrjs.com/docs/v2/reference/storable#method-decorator) decorator.\n\nFor example, here is how you would define a `Movie` component with some storable attributes and methods:\n\n```\n// JS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute, method} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  @primaryIdentifier() id;\n\n  @attribute('string') title = '';\n\n  @attribute('string') country = '';\n\n  @attribute('Date') releaseDate;\n\n  @method() async wasReleasedIn(year) {\n    await this.load({releaseDate});\n\n    return this.releaseDate().getFullYear() === year;\n  }\n}\n```\n\n```\n// TS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute, method} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  @primaryIdentifier() id!: string;\n\n  @attribute('string') title = '';\n\n  @attribute('string') country = '';\n\n  @attribute('Date') releaseDate!: Date;\n\n  @method() async wasReleasedIn(year: number) {\n    await this.load({releaseDate});\n\n    return this.releaseDate().getUTCFullYear() === year;\n  }\n}\n```\n\nNotice the `wasReleasedIn()` method that allows us to determine if a movie was released in a specific year. We could use this method as follows:\n\n```\nconst movie = new Movie({\n  title: 'Inception',\n  country: 'USA',\n  releaseDate: new Date('2010-07-16')\n});\n\nawait movie.wasReleasedIn(2010); // => true\nawait movie.wasReleasedIn(2011); // => false\n```\n\nSo far, there is nothing special about the `wasReleasedIn()` method. We could have achieved the same result without the [`@method()`](https://layrjs.com/docs/v2/reference/storable#method-decorator) decorator.\n\nNow, let's imagine that we want to find all the movies that was released in 2010. We could do so as follows:\n\n```\nawait Movie.find({\n  releaseDate: {\n    $greaterThanOrEqual: new Date('2010-01-01'),\n    $lessThan: new Date('2011-01-01')\n  }\n});\n```\n\nThat would certainly work, but wouldn't it be great if we could do the following instead:\n\n```\nawait Movie.find({wasReleasedIn: 2010});\n```\n\nUnfortunately, the above [`Query`](https://layrjs.com/docs/v2/reference/query) wouldn't work. To make such a query possible, we must somehow transform the logic of the `wasReleasedIn()` method into a regular query, and this is exactly where a `StorableMethod` can be useful.\n\nBecause the `wasReleasedIn()` method is a `StorableMethod` (thanks to the [`@method()`](https://layrjs.com/docs/v2/reference/storable#method-decorator) decorator), we can can associate a [`Finder`](https://layrjs.com/docs/v2/reference/storable-property#finder-type) to it by adding the [`@finder()`](https://layrjs.com/docs/v2/reference/storable#finder-decorator) decorator:\n\n```\n// JS\n\n// ...\n\nimport {finder} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  @finder(function (year) {\n    return {\n      releaseDate: {\n        $greaterThanOrEqual: new Date(`${year}-01-01`),\n        $lessThan: new Date(`${year + 1}-01-01`)\n      }\n    };\n  })\n  @method()\n  async wasReleasedIn(year) {\n    // ...\n  }\n}\n```\n\n```\n// TS\n\n// ...\n\nimport {finder} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  // ...\n\n  @finder(function (year: number) {\n    return {\n      releaseDate: {\n        $greaterThanOrEqual: new Date(`${year}-01-01`),\n        $lessThan: new Date(`${year + 1}-01-01`)\n      }\n    };\n  })\n  @method()\n  async wasReleasedIn(year: number) {\n    // ...\n  }\n}\n```\n\nAnd now, it is possible to use the `wasReleasedIn()` method in any query:\n\n```\n// Find all the movies released in 2010\nawait Movie.find({wasReleasedIn: 2010});\n\n// Find all the American movies released in 2010\nawait Movie.find({country: 'USA', wasReleasedIn: 2010});\n```\n\n#### Creation\n\n##### `new StorableMethod(name, parent, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates a storable method. Typically, instead of using this constructor, you would rather use the [`@method()`](https://layrjs.com/docs/v2/reference/storable#method-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the method.\n* `parent`: The [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class) class, prototype, or instance that owns the method.\n* `options`: An object specifying any option supported by the constructor of [`Method`](https://layrjs.com/docs/v2/reference/method#constructor) and [`StorableProperty`](https://layrjs.com/docs/v2/reference/storable-property#constructor).\n\n**Returns:**\n\nThe [`StorableMethod`](https://layrjs.com/docs/v2/reference/storable-method) instance that was created.\n\n#### Property Methods\n\nSee the methods that are inherited from the [`Property`](https://layrjs.com/docs/v2/reference/property#basic-methods) class.\n\n#### Finder\n\nSee the methods that are inherited from the [`StorableProperty`](https://layrjs.com/docs/v2/reference/storable-property#finder) class.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/storable-primary-identifier-attribute-7qq06XjPnNRXumxcngJvZv.immutable.md",
    "content": "### StorablePrimaryIdentifierAttribute <badge type=\"primary\">class</badge> {#storable-primary-identifier-attribute-class}\n\n*Inherits from [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute) and [`StorableAttribute`](https://layrjs.com/docs/v2/reference/storable-attribute).*\n\nThe `StorablePrimaryIdentifierAttribute` class is like the [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute) class but extended with the capabilities of the [`StorableAttribute`](https://layrjs.com/docs/v2/reference/storable-attribute) class.\n\n#### Usage\n\nTypically, you create a `StorablePrimaryIdentifierAttribute` and associate it to a [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class) using the [`@primaryIdentifier()`](https://layrjs.com/docs/v2/reference/storable#primary-identifier-decorator) decorator.\n\n**Example:**\n\n```\n// JS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  @primaryIdentifier() id;\n\n  @attribute('string') title = '';\n}\n```\n\n```\n// TS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  @primaryIdentifier() id!: string;\n\n  @attribute('string') title = '';\n}\n```\n\n#### Creation\n\n##### `new StorablePrimaryIdentifierAttribute(name, parent, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates a storable primary identifier attribute. Typically, instead of using this constructor, you would rather use the [`@primaryIdentifier()`](https://layrjs.com/docs/v2/reference/storable#primary-identifier-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the attribute.\n* `parent`: The [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class) prototype that owns the attribute.\n* `options`: An object specifying any option supported by the constructor of [`PrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/primary-identifier-attribute#constructor) and [`StorableAttribute`](https://layrjs.com/docs/v2/reference/storable-attribute#constructor).\n\n**Returns:**\n\nThe [`StorablePrimaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/storable-primary-identifier-attribute) instance that was created.\n\n#### Property Methods\n\nSee the methods that are inherited from the [`Property`](https://layrjs.com/docs/v2/reference/property#basic-methods) class.\n\n#### Attribute Methods\n\nSee the methods that are inherited from the [`Attribute`](https://layrjs.com/docs/v2/reference/attribute#value-type) class.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/storable-property-6Ooq1wLy5pGx7Jb1qRrRyf.immutable.md",
    "content": "### StorableProperty <badge type=\"primary\">class</badge> {#storable-property-class}\n\n*Inherits from [`Property`](https://layrjs.com/docs/v2/reference/property).*\n\nA base class from which classes such as [`StorableAttribute`](https://layrjs.com/docs/v2/reference/storable-attribute) or [`StorableMethod`](https://layrjs.com/docs/v2/reference/storable-method) are constructed. Unless you build a custom property class, you probably won't have to use this class directly.\n\n#### Creation\n\n##### `new StorableProperty(name, parent, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates a storable property.\n\n**Parameters:**\n\n* `name`: The name of the property.\n* `parent`: The [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class) class, prototype, or instance that owns the property.\n* `options`:\n  * `finder`: A function specifying a [`Finder`](https://layrjs.com/docs/v2/reference/storable-property#finder-type) for the property.\n  * `exposure`: A [`PropertyExposure`](https://layrjs.com/docs/v2/reference/property#property-exposure-type) object specifying how the property should be exposed to remote access.\n\n**Returns:**\n\nThe [`StorableProperty`](https://layrjs.com/docs/v2/reference/storable-property) instance that was created.\n\n#### Property Methods\n\nSee the methods that are inherited from the [`Property`](https://layrjs.com/docs/v2/reference/property#basic-methods) class.\n\n#### Finder\n\n##### `getFinder()` <badge type=\"secondary-outline\">instance method</badge> {#get-finder-instance-method}\n\nReturns the [`Finder`](https://layrjs.com/docs/v2/reference/storable-property#finder-type)of  the property.\n\n**Returns:**\n\nA [`Finder`](https://layrjs.com/docs/v2/reference/storable-property#finder-type) function (or `undefined` if the property has no associated finder).\n\n##### `hasFinder()` <badge type=\"secondary-outline\">instance method</badge> {#has-finder-instance-method}\n\nReturns whether the property has a [`Finder`](https://layrjs.com/docs/v2/reference/storable-property#finder-type).\n\n**Returns:**\n\nA boolean.\n\n##### `setFinder(finder)` <badge type=\"secondary-outline\">instance method</badge> {#set-finder-instance-method}\n\nSets a [`Finder`](https://layrjs.com/docs/v2/reference/storable-property#finder-type) for the property.\n\n**Parameters:**\n\n* `finder`: The [`Finder`](https://layrjs.com/docs/v2/reference/storable-property#finder-type) function to set.\n\n##### `Finder` <badge type=\"primary-outline\">type</badge> {#finder-type}\n\nA function representing the \"finder\" of a property.\n\nThe function should return a [`Query`](https://layrjs.com/docs/v2/reference/query) for the property that is queried for.\n\nThe function has the following characteristics:\n\n- It can be `async`.\n- As first parameter, it receives the value that was specified in the user's query.\n- It is executed with the parent of the property as `this` context.\n\nSee an example of use in the [`StorableAttribute`](https://layrjs.com/docs/v2/reference/storable-attribute) and [`StorableMethod`](https://layrjs.com/docs/v2/reference/storable-method) classes.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/storable-secondary-identifier-attribute-1TKIxZX9JZ2aGFU5iEmUSv.immutable.md",
    "content": "### StorableSecondaryIdentifierAttribute <badge type=\"primary\">class</badge> {#storable-secondary-identifier-attribute-class}\n\n*Inherits from [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute) and [`StorableAttribute`](https://layrjs.com/docs/v2/reference/storable-attribute).*\n\nThe `StorableSecondaryIdentifierAttribute` class is like the [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute) class but extended with the capabilities of the [`StorableAttribute`](https://layrjs.com/docs/v2/reference/storable-attribute) class.\n\n#### Usage\n\nTypically, you create a `StorableSecondaryIdentifierAttribute` and associate it to a [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class) using the [`@secondaryIdentifier()`](https://layrjs.com/docs/v2/reference/storable#secondary-identifier-decorator) decorator.\n\n**Example:**\n\n```\n// JS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, secondaryIdentifier, attribute} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  @primaryIdentifier() id;\n\n  @secondaryIdentifier() slug;\n\n  @attribute('string') title = '';\n}\n```\n\n```\n// TS\n\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, secondaryIdentifier, attribute} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  @primaryIdentifier() id!: string;\n\n  @secondaryIdentifier() slug!: string;\n\n  @attribute('string') title = '';\n}\n```\n\n#### Creation\n\n##### `new StorableSecondaryIdentifierAttribute(name, parent, [options])` <badge type=\"secondary\">constructor</badge> {#constructor}\n\nCreates a storable secondary identifier attribute. Typically, instead of using this constructor, you would rather use the [`@secondaryIdentifier()`](https://layrjs.com/docs/v2/reference/storable#secondary-identifier-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the attribute.\n* `parent`: The [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class) prototype that owns the attribute.\n* `options`: An object specifying any option supported by the constructor of [`SecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/secondary-identifier-attribute#constructor) and [`StorableAttribute`](https://layrjs.com/docs/v2/reference/storable-attribute#constructor).\n\n**Returns:**\n\nThe [`StorableSecondaryIdentifierAttribute`](https://layrjs.com/docs/v2/reference/storable-secondary-identifier-attribute) instance that was created.\n\n#### Property Methods\n\nSee the methods that are inherited from the [`Property`](https://layrjs.com/docs/v2/reference/property#basic-methods) class.\n\n#### Attribute Methods\n\nSee the methods that are inherited from the [`Attribute`](https://layrjs.com/docs/v2/reference/attribute#value-type) class.\n\n#### Observability\n\nSee the methods that are inherited from the [`Observable`](https://layrjs.com/docs/v2/reference/observable#observable-class) class.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/store-1mGBVGCiO4K5X9dBakik9Z.immutable.md",
    "content": "### Store <badge type=\"primary\">class</badge> {#store-class}\n\nAn abstract class from which classes such as [`MongoDBStore`](https://layrjs.com/docs/v2/reference/mongodb-store) or [`MemoryStore`](https://layrjs.com/docs/v2/reference/memory-store) are constructed. Unless you build a custom store, you probably won't have to use this class directly.\n\n#### Component Registration\n\n##### `registerRootComponent(rootComponent)` <badge type=\"secondary-outline\">instance method</badge> {#register-root-component-instance-method}\n\nRegisters all the [storable components](https://layrjs.com/docs/v2/reference/storable#storable-component-class) that are provided (directly or recursively) by the specified root component.\n\n**Parameters:**\n\n* `rootComponent`: A [`Component`](https://layrjs.com/docs/v2/reference/component) class.\n\n**Example:**\n\n```\nimport {Component} from '@layr/component';\nimport {Storable} from '@layr/storable';\nimport {MongoDBStore} from '@layr/mongodb-store';\n\nclass User extends Storable(Component) {\n  // ...\n}\n\nclass Movie extends Storable(Component) {\n  // ...\n}\n\nclass Application extends Component {\n  @provide() static User = User;\n  @provide() static Movie = Movie;\n}\n\nconst store = new MongoDBStore('mongodb://user:pass@host:port/db');\n\nstore.registerRootComponent(Application); // User and Movie will be registered\n```\n\n##### `getRootComponents()` <badge type=\"secondary-outline\">instance method</badge> {#get-root-components-instance-method}\n\nGets all the root components that are registered into the store.\n\n**Returns:**\n\nAn iterator of [`Component`](https://layrjs.com/docs/v2/reference/component) classes.\n\n##### `getStorable(name)` <badge type=\"secondary-outline\">instance method</badge> {#get-storable-instance-method}\n\nGets a [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class) that is registered into the store. An error is thrown if there is no storable component with the specified name.\n\n**Parameters:**\n\n* `name`: The name of the storable component to get.\n\n**Returns:**\n\nA [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storable-component-class) class.\n\n**Example:**\n\n```\n// See the definition of `store` in the `registerRootComponent()` example\n\nstore.getStorable('Movie'); // => Movie class\nstore.getStorable('User'); // => User class\nstore.getStorable('Film'); // => Error\n```\n\n##### `hasStorable(name)` <badge type=\"secondary-outline\">instance method</badge> {#has-storable-instance-method}\n\nReturns whether a [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class) is registered into the store.\n\n**Parameters:**\n\n* `name`: The name of the storable component to check.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\n// See the definition of `store` in the `registerRootComponent()` example\n\nstore.hasStorable('Movie'); // => true\nstore.hasStorable('User'); // => true\nstore.hasStorable('Film'); // => false\n```\n\n##### `registerStorable(storable)` <badge type=\"secondary-outline\">instance method</badge> {#register-storable-instance-method}\n\nRegisters a specific [storable component](https://layrjs.com/docs/v2/reference/storable#storable-component-class) into the store. Typically, instead of using this method, you would rather use the [`registerRootComponent()`](https://layrjs.com/docs/v2/reference/store#register-root-component-instance-method) method to register multiple storable components at once.\n\n**Parameters:**\n\n* `storable`: The [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storable-component-class) class to register.\n\n**Example:**\n\n```\nclass Movie extends Storable(Component) {\n  // ...\n}\n\nconst store = new MongoDBStore('mongodb://user:pass@host:port/db');\n\nstore.registerStorable(Movie);\n```\n\n##### `getStorables()` <badge type=\"secondary-outline\">instance method</badge> {#get-storables-instance-method}\n\nGets all the [storable components](https://layrjs.com/docs/v2/reference/storable#storable-component-class) that are registered into the store.\n\n**Returns:**\n\nAn iterator of [`StorableComponent`](https://layrjs.com/docs/v2/reference/storable#storable-component-class) classes.\n\n#### Migration\n\n##### `migrateStorables([options])` <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">async</badge> {#migrate-storables-instance-method}\n\nMigrates the database to reflect all the [storable components](https://layrjs.com/docs/v2/reference/storable#storable-component-class) that are registered into the store.\n\nThe migration consists in synchronizing the indexes of the database with the indexes that are defined in each storable component (typically using the [`@index()`](https://layrjs.com/docs/v2/reference/storable#index-decorator) decorator).\n\n**Parameters:**\n\n* `options`:\n  * `silent`: A boolean specifying whether the operation should not produce any output in the console (default: `false`).\n\n**Example:**\n\nSee an example of use in the [`Index`](https://layrjs.com/docs/v2/reference/index) class."
  },
  {
    "path": "website/frontend/public/docs/v2/reference/validator-2aTKblcN30ADdXqrI3bHXv.immutable.md",
    "content": "### Validator <badge type=\"primary\">class</badge> {#validator-class}\n\nA class to handle the validation of the component attributes.\n\n#### Usage\n\nYou shouldn't have to create a `Validator` instance directly. Instead, when you define an attribute (using a decorator such as [`@attribute()`](https://layrjs.com/docs/v2/reference/component#attribute-decorator)), you can invoke some [built-in validator builders](https://layrjs.com/docs/v2/reference/validator#built-in-validator-builders) or specify your own [custom validation functions](https://layrjs.com/docs/v2/reference/validator#custom-validation-functions) that will be automatically transformed into `Validator` instances.\n\n**Example:**\n\n```\n// JS\n\nimport {Component, attribute, validators} from '@layr/component';\n\nconst {notEmpty, maxLength, integer, greaterThan} = validators;\n\nclass Movie extends Component {\n  // An attribute of type 'string' that cannot be empty or exceed 30 characters\n  @attribute('string', {validators: [notEmpty(), maxLength(30)]}) title;\n\n  // An attribute of type 'number' that must an integer greater than 0\n  @attribute('number', {validators: [integer(), greaterThan(0)]}) reference;\n\n  // An array attribute that can contain up to 5 non-empty strings\n  @attribute('string[]', {validators: [maxLength(5)], items: {validators: [notEmpty()]}})\n  tags = [];\n}\n```\n\n```\n// TS\n\nimport {Component, attribute, validators} from '@layr/component';\n\nconst {notEmpty, maxLength, integer, greaterThan} = validators;\n\nclass Movie extends Component {\n  // An attribute of type 'string' that cannot be empty or exceed 30 characters\n  @attribute('string', {validators: [notEmpty(), maxLength(30)]}) title!: string;\n\n  // An attribute of type 'number' that must an integer greater than 0\n  @attribute('number', {validators: [integer(), greaterThan(0)]}) reference!: number;\n\n  // An array attribute that can contain up to 5 non-empty strings\n  @attribute('string[]', {validators: [maxLength(5)], items: {validators: [notEmpty()]}})\n  tags: string[] = [];\n}\n```\n\nIn case you want to access the `Validator` instances that were created under the hood, you can do the following:\n\n```\nconst movie = new Movie({ ... });\n\nmovie.getAttribute('title').getValueType().getValidators();\n// => [notEmptyValidator, maxLengthValidator]\n\nmovie.getAttribute('reference').getValueType().getValidators();\n// => [integerValidator, greaterThanValidator]\n\nmovie.getAttribute('tags').getValueType().getValidators();\n// => [maxLengthValidator]\n\nmovie.getAttribute('tags').getValueType().getItemType().getValidators();\n// => [notEmptyValidator]\n```\n\n#### Built-In Validator Builders\n\nLayr provides a number of validator builders that can be used when you define your component attributes. See an [example of use](https://layrjs.com/docs/v2/reference/validator#usage) above.\n\n##### Numbers\n\nThe following validator builders can be used to validate numbers:\n\n* `integer()`: Ensures that a number is an integer.\n* `positive()`: Ensures that a number is greater than or equal to 0.\n* `negative()`: Ensures that a number is less than 0.\n* `lessThan(value)`: Ensures that a number is less than the specified value.\n* `lessThanOrEqual(value)`: Ensures that a number is less than or equal to the specified value.\n* `greaterThan(value)`: Ensures that a number is greater than the specified value.\n* `greaterThanOrEqual(value)`: Ensures that a number is greater than or equal to the specified value.\n* `range([min, max])`: Ensures that a number is in the specified inclusive range.\n* `anyOf(arrayOfNumbers)`: Ensures that a number is any of the specified numbers.\n* `noneOf(arrayOfNumbers)`: Ensures that a number is none of the specified numbers.\n\n##### Strings\n\nThe following validator builders can be used to validate strings:\n\n* `notEmpty()`: Ensures that a string is not empty.\n* `minLength(value)`: Ensures that a string has at least the specified number of characters.\n* `maxLength(value)`: Ensures that a string doesn't exceed the specified number of characters.\n* `rangeLength([min, max])`: Ensures that the length of a string is in the specified inclusive range.\n* `match(regExp)`: Ensures that a string matches the specified [regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions).\n* `anyOf(arrayOfStrings)`: Ensures that a string is any of the specified strings.\n* `noneOf(arrayOfStrings)`: Ensures that a string is none of the specified strings.\n\n##### Arrays\n\nThe following validator builders can be used to validate arrays:\n\n* `notEmpty()`: Ensures that an array is not empty.\n* `minLength(value)`: Ensures that an array has at least the specified number of items.\n* `maxLength(value)`: Ensures that an array doesn't exceed the specified number of items.\n* `rangeLength([min, max])`: Ensures that the length of an array is in the specified inclusive range.\n\n##### Any Type\n\nThe following validator builder can be used to validate any type of values:\n\n* `required()`: Ensures that a value is not undefined.\n* `missing()`: Ensures that a value is undefined.\n\n##### Validator Operators\n\nYou can compose several validators using some validator operators:\n\n* `either(arrayOfValidators)`: Performs a logical **OR** operation on an array of validators.\n* `optional(validatorOrArrayOfValidators)`: If a value is is not undefined, ensures that it satisfies the specified validators (can be a single validator or an array of validators).\n\n##### Custom Failed Validation Message\n\nYou can pass an additional parameter to all the built-in validators builder to customize the message of the error that is thrown in case of failed validation.\n\n**Example:**\n\n```\nmaxLength(16, 'A username cannot exceed 16 characters');\n```\n\n#### Custom Validation Functions\n\nIn addition to the [built-in validator builders](https://layrjs.com/docs/v2/reference/validator#built-in-validator-builders), you can validate your component attributes with your own custom validation functions.\n\nA custom validation function takes a value as first parameter and returns a boolean indicating whether the validation has succeeded or not.\n\n**Example:**\n\n```\n// JS\n\nimport {Component, attribute} from '@layr/component';\n\nclass OddNumber extends Component {\n  // Ensures that the value is an odd number\n  @attribute('number', {validators: [(value) => value % 2 === 1]}) value;\n}\n```\n\n```\n// TS\n\nimport {Component, attribute} from '@layr/component';\n\nclass OddNumber extends Component {\n  // Ensures that the value is an odd number\n  @attribute('number', {validators: [(value) => value % 2 === 1]}) value!: number;\n}\n```\n\n#### Methods\n\n##### `getFunction()` <badge type=\"secondary-outline\">instance method</badge> {#get-function-instance-method}\n\nReturns the function associated to the validator.\n\n**Returns:**\n\nA function.\n\n**Example:**\n\n```\nmaxLength(8).getFunction();\n// => function (value, maxLength) { return value.length <= maxLength; }\n```\n\n##### `getName()` <badge type=\"secondary-outline\">instance method</badge> {#get-name-instance-method}\n\nReturns the name of the validator.\n\n**Returns:**\n\nA string.\n\n**Example:**\n\n```\nmaxLength(8).getName(); // => 'maxLength'\n```\n\n##### `getArguments()` <badge type=\"secondary-outline\">instance method</badge> {#get-arguments-instance-method}\n\nReturns the arguments of the validator.\n\n**Returns:**\n\nAn array of values of any type.\n\n**Example:**\n\n```\nmaxLength(8).getArguments(); // => [8]\n```\n\n##### `getMessage()` <badge type=\"secondary-outline\">instance method</badge> {#get-message-instance-method}\n\nReturns the message of the error that is thrown in case of failed validation.\n\n**Returns:**\n\nA string.\n\n**Example:**\n\n```\nmaxLength(8).getMessage(); // => 'The validator maxLength(8) failed'\n```\n\n##### `run()` <badge type=\"secondary-outline\">instance method</badge> {#run-instance-method}\n\nRuns the validator against the specified value.\n\n**Returns:**\n\n`true` if the validation has succeeded, `false` otherwise.\n\n**Example:**\n\n```\nmaxLength(8).run('1234567'); // => true\nmaxLength(8).run('12345678'); // => true\nmaxLength(8).run('123456789'); // => false\n```\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/value-type-FqKdGxNpEPIDVR56kq4NN.immutable.md",
    "content": "### ValueType <badge type=\"primary\">class</badge> {#value-type-class}\n\nA class to handle the various types of values supported by Layr.\n\n#### Usage\n\nYou shouldn't have to create a `ValueType` instance directly. Instead, when you define an attribute (using a decorator such as [`@attribute()`](https://layrjs.com/docs/v2/reference/component#attribute-decorator)), you can specify a string representing a type of value, and a `ValueType` will be automatically created for you.\n\n**Example:**\n\n```\n// JS\n\nimport {Component, attribute, validators} from '@layr/component';\n\nconst {integer, greaterThan} = validators;\n\nclass Movie extends Component {\n  // Required 'string' attribute\n  @attribute('string') title;\n\n  // Required 'number' attribute with some validators\n  @attribute('number', {validators: [integer(), greaterThan(0)]}) reference;\n\n  // Optional 'string' attribute\n  @attribute('string?') summary;\n\n  // Required 'Director' attribute\n  @attribute('Director') director;\n\n  // Required array of 'Actor' attribute with a default value\n  @attribute('Actor[]') actors = [];\n}\n```\n\n```\n// TS\n\nimport {Component, attribute, validators} from '@layr/component';\n\nconst {integer, greaterThan} = validators;\n\nclass Movie extends Component {\n  // Required 'string' attribute\n  @attribute('string') title!: string;\n\n  // Required 'number' attribute with some validators\n  @attribute('number', {validators: [integer(), greaterThan(0)]}) reference!: number;\n\n  // Optional 'string' attribute\n  @attribute('string?') summary?: string;\n\n  // Required 'Director' attribute\n  @attribute('Director') director!: Director;\n\n  // Required array of 'Actor' attribute with a default value\n  @attribute('Actor[]') actors: Actor[] = [];\n}\n```\n\nIn case you want to access the `ValueType` instances that were created under the hood, you can do the following:\n\n```\nconst movie = new Movie({ ... });\n\nlet valueType = movie.getAttribute('title').getValueType();\nvalueType.toString(); // => 'string'\n\nvalueType = movie.getAttribute('reference').getValueType();\nvalueType.toString(); // => 'number'\nvalueType.getValidators(); // => [integerValidator, greaterThanValidator]\n\nvalueType = movie.getAttribute('summary').getValueType();\nvalueType.toString(); // => 'string?'\nvalueType.isOptional(); // => true\n\nvalueType = movie.getAttribute('director').getValueType();\nvalueType.toString(); // => 'Director'\n\nvalueType = movie.getAttribute('actors').getValueType();\nvalueType.toString(); // => 'Actor[]'\nconst itemValueType = valueType.getItemType(); // => A ValueType representing the type of the items inside the array\nitemValueType.toString(); // => Actor\n```\n\n#### Supported Types\n\nLayr supports a number of types that can be represented by a string in a way that is very similar to the way you specify basic types in [TypeScript](https://www.typescriptlang.org/).\n\n##### Scalars\n\nTo specify a scalar type, simply specify a string representing it:\n\n* `'boolean'`: A boolean.\n* `'number'`: A floating-point number.\n* `'string'`: A string.\n\n##### Arrays\n\nTo specify an array type, add `'[]'` after any other type:\n\n* `'number[]'`: An array of numbers.\n* `'string[]'`: An array of strings.\n* `'Actor[]'`: An array of `Actor`.\n* `'number[][]'`: A matrix of numbers.\n\n##### Objects\n\nTo specify a plain object type, just specify the string `'object'`:\n\n* `'object'`: A plain JavaScript object.\n\nSome common JavaScript objects are supported as well:\n\n* `'Date'`: A JavaScript `Date` instance.\n* `'RegExp'`: A JavaScript `RegExp` instance.\n\n##### Components\n\nAn attribute can hold a reference to a [`Component`](https://layrjs.com/docs/v2/reference/component) instance, or contain an [`EmbeddedComponent`](https://layrjs.com/docs/v2/reference/embedded-component) instance. To specify such a type, just specify the name of the component:\n\n* `'Director'`: A reference to a `Director` component instance.\n* `'MovieDetails'`: A `MovieDetails` embedded component instance.\n\nIt is also possible to specify a type that represents a reference to a [`Component`](https://layrjs.com/docs/v2/reference/component) class. To do so, add `'typeof '` before the name of the component:\n\n* `'typeof Director'`: A reference to the `Director` component class.\n\n##### `'?'` Modifier\n\nBy default, all attribute values are required, which means a value cannot be `undefined`. To make a value optional, add a question mark (`'?'`) after its type:\n\n* `'string?'`: A string or `undefined`.\n* `'number[]?'`: A number array or `undefined`.\n* `'number?[]'`: An array containing some values of type number or `undefined`.\n* `'Director?'`: A reference to a `Director` component instance or `undefined`.\n\n##### '`any`' Type\n\nIn some rare occasions, you may want to define an attribute that can handle any type of values. To do so, you can specify the string `'any'`:\n\n* `'any'`: Any type of values.\n\n#### Methods\n\n##### `isOptional()` <badge type=\"secondary-outline\">instance method</badge> {#is-optional-instance-method}\n\nReturns whether the value type is marked as optional. A value of a type marked as optional can be `undefined`.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nmovie.getAttribute('summary').getValueType().isOptional(); // => true\nmovie.summary = undefined; // Okay\n\nmovie.getAttribute('title').getValueType().isOptional(); // => false\nmovie.title = undefined; // Error\n```\n\n##### `getValidators()` <badge type=\"secondary-outline\">instance method</badge> {#get-validators-instance-method}\n\nReturns the validators associated to the value type.\n\n**Returns:**\n\nA array of [`Validator`](https://layrjs.com/docs/v2/reference/component).\n\n**Example:**\n\n```\nmovie.getAttribute('reference').getValueType().getValidators();\n// => [integerValidator, greaterThanValidator]\n```\n\n##### `getItemType()` <badge type=\"secondary-outline\">instance method</badge> {#get-item-type-instance-method}\n\nIn case the value type is an array, returns the value type of the items it contains.\n\n**Returns:**\n\nA [ValueType](https://layrjs.com/docs/v2/reference/value-type).\n\n**Example:**\n\n```\nmovie.getAttribute('actors').getValueType().getItemType().toString(); // => 'Actor'\n```\n\n##### `toString()` <badge type=\"secondary-outline\">instance method</badge> {#to-string-instance-method}\n\nReturns a string representation of the value type.\n\n**Returns:**\n\nA string.\n\n**Example:**\n\n```\nmovie.getAttribute('title').getValueType().toString(); // => 'string'\nmovie.getAttribute('summary').getValueType().toString(); // => 'string?'\nmovie.getAttribute('actors').getValueType().toString(); // => 'Actor[]'\n```\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/with-roles-CcLPnrWKVYxnZXgNTqVYi.immutable.md",
    "content": "### WithRoles() <badge type=\"primary\">mixin</badge> {#with-roles-mixin}\n\nExtends a [`Component`](https://layrjs.com/docs/v2/reference/component) class with the ability to handle [roles](https://layrjs.com/docs/v2/reference/role).\n\n#### Usage\n\nCall `WithRoles()` with a [`Component`](https://layrjs.com/docs/v2/reference/component) class to construct a [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class) class. Next, use the [`@role()`](https://layrjs.com/docs/v2/reference/with-roles#role-decorator) decorator to define some [roles](https://layrjs.com/docs/v2/reference/role). Lastly, use the [`@expose()`](https://layrjs.com/docs/v2/reference/component#expose-decorator) decorator to authorize some attributes or methods according to these roles.\n\n**Example:**\n\n```\nimport {Component, attribute, method, expose} from '@layr/component';\nimport {WithRoles, role} from '@layr/with-roles';\n\nclass Article extends WithRoles(Component) {\n  @role('admin') static adminRoleResolver() {\n    // ...\n  }\n\n  // Only readable by an administrator\n  @expose({get: 'admin'}) @attribute('number') viewCount = 0;\n\n  // Only callable by an administrator\n  @expose({call: 'admin'}) @method() unpublish() {\n    // ...\n  }\n}\n```\n\nOnce you have a [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class), you can use any method provided by the `WithRoles()` mixin. For example, you can use the [`resolveRole()`](https://layrjs.com/docs/v2/reference/with-roles#resolve-role-dual-method) method to determine if the user has a specific role:\n\n```\nArticle.resolveRole('admin'); // `true` or `false` depending on the current user\n```\n\nSee the [\"Handling Authorization\"](https://layrjs.com/docs/v2/introduction/handling-authorization) tutorial for a comprehensive example using the `WithRoles()` mixin.\n\n### ComponentWithRoles <badge type=\"primary\">class</badge> {#component-with-roles-class}\n\n*Inherits from [`Component`](https://layrjs.com/docs/v2/reference/component).*\n\nA `ComponentWithRoles` class is constructed by calling the `WithRoles()` mixin ([see above](https://layrjs.com/docs/v2/reference/with-roles#with-roles-mixin)).\n\n#### Roles\n\n##### `getRole(name, [options])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#get-role-dual-method}\n\nGets a role. If there is no role with the specified name, an error is thrown.\n\n**Parameters:**\n\n* `name`: The name of the role to get.\n* `options`:\n  * `fallbackToClass`: A boolean specifying whether the role should be get from the component class if there is no role with the specified name in the component prototype or instance (default: `false`).\n\n**Returns:**\n\nA [Role](https://layrjs.com/docs/v2/reference/role) instance.\n\n**Example:**\n\n```\nArticle.getRole('admin'); // => 'admin' role\n\nconst article = new Article();\narticle.getRole('admin'); // Error\narticle.getRole('admin', {fallbackToClass: true}); // => 'admin' role\n```\n\n##### `hasRole(name, [options])` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#has-role-dual-method}\n\nReturns whether the [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class) has a role with the specified name.\n\n**Parameters:**\n\n* `name`: The name of the role to check.\n* `options`:\n  * `fallbackToClass`: A boolean specifying whether the component class should be considered if there is no role with the specified name in the component prototype or instance (default: `false`).\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nArticle.hasRole('admin'); // => true\n\nconst article = new Article();\narticle.hasRole('admin'); // => false\narticle.hasRole('admin', {fallbackToClass: true}); // => true\n```\n\n##### `setRole(name, resolver)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> {#set-role-dual-method}\n\nSets a role in the current [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class) class or prototype.\n\nTypically, instead of using this method, you would rather use the [`@role()`](https://layrjs.com/docs/v2/reference/with-roles#role-decorator) decorator.\n\n**Parameters:**\n\n* `name`: The name of the role.\n* `resolver`: A function that should return a boolean indicating whether a user has the corresponding role. The function can be asynchronous and is called with the role's parent as `this` context.\n\n**Returns:**\n\nThe [Role](https://layrjs.com/docs/v2/reference/role) instance that was created.\n\n**Example:**\n\n```\nArticle.setRole('admin', function () {\n // ...\n});\n```\n\n##### `resolveRole(name)` <badge type=\"secondary\">class method</badge> <badge type=\"secondary-outline\">instance method</badge> <badge type=\"outline\">possibly async</badge> {#resolve-role-dual-method}\n\nResolves a role by calling its resolver function. If there is no role with the specified name in the [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class) class or prototype, an error is thrown.\n\nThe resolver function is called with the role's parent as `this` context.\n\nOnce a role has been resolved, the result is cached, so the resolver function is called only one time.\n\n**Parameters:**\n\n* `name`: The name of the role to resolve.\n\n**Returns:**\n\nA boolean.\n\n**Example:**\n\n```\nArticle.resolveRole('admin'); // `true` or `false` depending on the current user\n```\n\n#### Decorators\n\n##### `@role(name)` <badge type=\"tertiary\">decorator</badge> {#role-decorator}\n\nDefines a [role](https://layrjs.com/docs/v2/reference/role) in a [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class) class or prototype.\n\nThis decorator should be used to decorate a class or instance method that implements the role's resolver. The method can be asynchronous and should return a boolean indicating whether a user has the corresponding role.\n\n**Parameters:**\n\n* `name`: The name of the role to define.\n\n**Example:**\n\nSee an example of use in the [`WithRoles()`](https://layrjs.com/docs/v2/reference/with-roles#with-roles-mixin) mixin.\n#### Utilities\n\n##### `isComponentWithRolesClassOrInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-component-with-roles-class-or-instance-function}\n\nReturns whether the specified value is a [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class) class or instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `assertIsComponentWithRolesClassOrInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#assert-is-component-with-roles-class-or-instance-function}\n\nThrows an error if the specified value is not a [`ComponentWithRoles`](https://layrjs.com/docs/v2/reference/with-roles#component-with-roles-class) class or instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n"
  },
  {
    "path": "website/frontend/public/docs/v2/reference/wrapper-4uhJFGS18pbZTt9qdtLnL2.immutable.md",
    "content": "### Wrapper <badge type=\"primary\">class</badge> {#wrapper-class}\n\n*Inherits from [`Addressable`](https://layrjs.com/docs/v2/reference/addressable).*\n\nRepresents a wrapper in a [routable component](https://layrjs.com/docs/v2/reference/routable#routable-component-class).\n\n#### Usage\n\nTypically, you create a `Wrapper` and associate it to a routable component by using the [`@wrapper()`](https://layrjs.com/docs/v2/reference/routable#wrapper-decorator) decorator.\n\nSee an example of use in the [`BrowserNavigatorView`](https://layrjs.com/docs/v2/reference/react-integration#browser-navigator-view-react-component) React component.\n\n#### Creation\n\nSee the constructor that is inherited from the [`Addressable`](https://layrjs.com/docs/v2/reference/addressable#constructor) class.\n\n#### Methods\n\nSee the methods that are inherited from the [`Addressable`](https://layrjs.com/docs/v2/reference/addressable#basic-methods) class.\n\n#### Types\n\nSee the types that are related to the [`Addressable`](https://layrjs.com/docs/v2/reference/addressable#types) class.\n\n#### Utilities\n\n##### `isWrapperClass(value)` <badge type=\"tertiary-outline\">function</badge> {#is-wrapper-class-function}\n\nReturns whether the specified value is a [`Wrapper`](https://layrjs.com/docs/v2/reference/wrapper) class.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n\n##### `isWrapperInstance(value)` <badge type=\"tertiary-outline\">function</badge> {#is-wrapper-instance-function}\n\nReturns whether the specified value is a [`Wrapper`](https://layrjs.com/docs/v2/reference/wrapper) instance.\n\n**Parameters:**\n\n* `value`: A value of any type.\n\n**Returns:**\n\nA boolean.\n"
  },
  {
    "path": "website/frontend/src/components/application.tsx",
    "content": "import {provide} from '@layr/component';\nimport {Routable} from '@layr/routable';\nimport {useState} from 'react';\nimport {layout, page, view, Customizer} from '@layr/react-integration';\nimport {jsx, useTheme} from '@emotion/react';\nimport {EmotionStarter} from '@emotion-starter/react';\nimport {EmotionKit, Container, Stack} from '@emotion-kit/react';\n\nimport type {Application as BackendApplication} from '../../../backend/src/components/application';\nimport {Home} from './home';\nimport {Docs, LAST_VERSION} from './docs';\nimport {extendUser} from './user';\nimport {Blog} from './blog';\nimport {extendArticle} from './article';\nimport {extendNewsletter} from './newsletter';\nimport {getGlobalStyles, useStyles} from '../styles';\nimport {FullHeight, ErrorMessage, LoadingSpinner} from '../ui';\n\nimport layrLogo from '../assets/layr-logo-with-icon-dark-mode.svg';\nimport love from '../assets/f-plus-b-equals-love.svg';\nimport pageNotFound from '../assets/page-not-found.png';\n\nexport const extendApplication = (Base: typeof BackendApplication) => {\n  class Application extends Routable(Base) {\n    @provide() static User = extendUser(Base.User);\n    @provide() static Home = Home;\n    @provide() static Docs = Docs;\n    @provide() static Blog = Blog;\n    @provide() static Article = extendArticle(Base.Article);\n    @provide() static Newsletter = extendNewsletter(Base.Newsletter);\n\n    @layout('') static RootLayout({children}: {children: () => any}) {\n      return (\n        <EmotionStarter\n          mode={'dark'}\n          theme={{\n            fontFamilies: {body: \"'Open Sans', sans-serif\", heading: \"'Open Sans', sans-serif\"}\n          }}\n          globalStylesGetter={getGlobalStyles}\n        >\n          <EmotionKit>\n            <Customizer\n              dataPlaceholder={() => <LoadingSpinner />}\n              errorRenderer={(error) => <ErrorMessage>{error}</ErrorMessage>}\n            >\n              {children()}\n            </Customizer>\n          </EmotionKit>\n        </EmotionStarter>\n      );\n    }\n\n    @layout('[]/') static MainLayout({children}: {children: () => any}) {\n      const theme = useTheme();\n\n      return (\n        <FullHeight css={{display: 'flex', flexDirection: 'column'}}>\n          <div>\n            <Container>\n              <this.HeaderView />\n            </Container>\n          </div>\n\n          <div\n            css={theme.responsive({\n              flexGrow: 1,\n              display: 'flex',\n              padding: ['2rem', , '2rem 15px'],\n              justifyContent: 'center'\n            })}\n          >\n            {children()}\n          </div>\n\n          <div css={{backgroundColor: theme.colors.background.highlighted}}>\n            <Container>\n              <this.FooterView />\n            </Container>\n          </div>\n        </FullHeight>\n      );\n    }\n\n    @view() static HeaderView() {\n      const {Home, Docs, Blog} = this;\n\n      const theme = useTheme();\n      const styles = useStyles();\n\n      return (\n        <header css={{minHeight: 60, padding: '.5rem 0', display: 'flex', alignItems: 'center'}}>\n          <Home.MainPage.Link css={{display: 'flex', flexDirection: 'column'}}>\n            <img src={layrLogo} alt=\"Layr\" css={{height: 32}} />\n          </Home.MainPage.Link>\n\n          <div css={{marginLeft: '0.6rem', marginTop: '7px'}}>\n            <small css={{color: theme.colors.text.muted, letterSpacing: '0.04rem'}}>\n              {LAST_VERSION}\n            </small>\n          </div>\n\n          <Stack spacing=\"1.5rem\" css={{marginLeft: 'auto', alignItems: 'center'}}>\n            <Docs.MainPage.Link>Docs</Docs.MainPage.Link>\n            <Blog.MainPage.Link>Blog</Blog.MainPage.Link>\n            <a\n              href=\"https://github.com/layrjs/layr\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              css={theme.responsive({\n                ...styles.menuItemLink,\n                display: ['inline-block', , , 'none']\n              })}\n            >\n              GitHub\n            </a>\n          </Stack>\n        </header>\n      );\n    }\n\n    @view() static FooterView() {\n      const {Docs, Blog} = this;\n\n      const theme = useTheme();\n      const styles = useStyles();\n\n      return (\n        <footer\n          css={{padding: '3rem 0', display: 'flex', flexDirection: 'column', alignItems: 'center'}}\n        >\n          <nav\n            css={theme.responsive({\n              width: '100%',\n              maxWidth: 200,\n              display: 'flex',\n              flexDirection: ['row', , , 'column'],\n              justifyContent: 'space-between',\n              lineHeight: theme.lineHeights.small\n            })}\n          >\n            <div>\n              <Stack direction=\"column\" css={theme.responsive({textAlign: [, , , 'center']})}>\n                <Docs.MainPage.Link>Docs</Docs.MainPage.Link>\n                <Blog.MainPage.Link>Blog</Blog.MainPage.Link>\n                <a\n                  href=\"mailto:hello@layrjs.com\"\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                  css={styles.menuItemLink}\n                >\n                  Contact\n                </a>\n              </Stack>\n            </div>\n\n            <div css={theme.responsive({marginTop: [, , , '1rem'], textAlign: [, , , 'center']})}>\n              <Stack direction=\"column\">\n                <a\n                  href=\"https://github.com/layrjs/layr\"\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                  css={styles.menuItemLink}\n                >\n                  GitHub\n                </a>\n                <a\n                  href=\"https://twitter.com/layrjs\"\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                  css={styles.menuItemLink}\n                >\n                  Twitter\n                </a>\n                <a\n                  href=\"https://www.youtube.com/channel/UCa2ZQbL0tlSR97GelMdgm6g\"\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                  css={styles.menuItemLink}\n                >\n                  YouTube\n                </a>\n              </Stack>\n            </div>\n          </nav>\n\n          <div css={{marginTop: '3rem'}}>\n            <small css={{color: theme.colors.text.muted}}>\n              A{' '}\n              <a href=\"https://1place.io\" target=\"_blank\" css={styles.hiddenLink}>\n                1Place\n              </a>{' '}\n              open source project.\n            </small>\n          </div>\n\n          <this.FrontendPlusBackendEqualsLoveView />\n        </footer>\n      );\n    }\n\n    @view() static FrontendPlusBackendEqualsLoveView() {\n      const theme = useTheme();\n\n      const [textMode, setTextMode] = useState(false);\n\n      return (\n        <div\n          onClick={() => {\n            setTextMode(!textMode);\n          }}\n          css={{\n            marginTop: '3rem',\n            height: '1.5rem',\n            display: 'flex',\n            alignItems: 'center',\n            justifyContent: 'center',\n            cursor: 'pointer'\n          }}\n        >\n          {!textMode ? (\n            <img\n              src={love}\n              alt=\"Frontend + Backend = Love\"\n              title=\"Frontend + Backend = Love\"\n              css={{width: 80}}\n            />\n          ) : (\n            <div>\n              <small style={{color: theme.colors.primary.normal}}>Frontend</small>\n              <small style={{color: theme.colors.text.muted}}> + </small>\n              <small style={{color: theme.colors.secondary.normal}}>Backend</small>\n              <small style={{color: theme.colors.text.muted}}> = </small>\n              <small style={{color: theme.colors.tertiary.normal}}>Love</small>\n            </div>\n          )}\n        </div>\n      );\n    }\n\n    @page('[/]*') static NotFoundPage() {\n      return <this.NotFoundView />;\n    }\n\n    @view() static NotFoundView() {\n      return (\n        <div\n          css={{\n            width: '100%',\n            padding: '3rem 15px',\n            display: 'flex',\n            flexDirection: 'column',\n            alignItems: 'center'\n          }}\n        >\n          <img src={pageNotFound} alt=\"Page not found\" css={{width: 350, maxWidth: '100%'}} />\n          <h4 css={{marginTop: '3rem', textAlign: 'center'}}>Sorry, there is nothing here.</h4>\n        </div>\n      );\n    }\n  }\n\n  return Application;\n};\n"
  },
  {
    "path": "website/frontend/src/components/article.tsx",
    "content": "import {consume} from '@layr/component';\nimport {Routable} from '@layr/routable';\nimport {layout, page, view, useData, useAction} from '@layr/react-integration';\nimport {Fragment, useMemo} from 'react';\nimport {jsx, useTheme} from '@emotion/react';\nimport {Input, TextArea, Button} from '@emotion-starter/react';\nimport {Stack} from '@emotion-kit/react';\nimport {format} from 'date-fns';\n\nimport type {extendUser} from './user';\nimport type {Article as BackendArticle} from '../../../backend/src/components/article';\nimport type {Home} from './home';\nimport {Title} from '../ui';\nimport {Markdown} from '../markdown';\n\nexport const extendArticle = (Base: typeof BackendArticle) => {\n  class Article extends Routable(Base) {\n    declare ['constructor']: typeof Article;\n\n    @consume() static User: ReturnType<typeof extendUser>;\n    @consume() static Home: typeof Home;\n\n    @layout('[/blog]/articles/:slug') ItemLayout({children}: {children: () => any}) {\n      return useData(\n        async () => {\n          await this.load({\n            title: true,\n            description: true,\n            body: true,\n            slug: true,\n            author: {fullName: true, url: true},\n            createdAt: true\n          });\n        },\n\n        children\n      );\n    }\n\n    @page('[/blog/articles/:slug]') ItemPage() {\n      return (\n        <>\n          <Title>{this.title}</Title>\n          <h2>{this.title}</h2>\n          <this.MetaView css={{marginTop: '-.75rem', marginBottom: '1.5rem'}} />\n          <Markdown>{this.body}</Markdown>\n        </>\n      );\n    }\n\n    @view() ListItemView() {\n      const theme = useTheme();\n\n      return (\n        <>\n          <this.ItemPage.Link>\n            <h4 css={{':hover': {color: theme.colors.primary.highlighted}}}>{this.title}</h4>\n          </this.ItemPage.Link>\n          <this.MetaView css={{marginTop: '-.75rem'}} />\n        </>\n      );\n    }\n\n    @view() MetaView({...props}) {\n      const theme = useTheme();\n\n      return (\n        <p css={{color: theme.colors.text.muted}} {...props}>\n          <span>{format(this.createdAt, 'MMMM do, y')}</span>\n          {', by '}\n          <a\n            href={this.author.url}\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            css={{color: theme.colors.text.muted}}\n          >\n            {this.author.fullName}\n          </a>\n        </p>\n      );\n    }\n\n    @page('[/blog/articles/:slug]/edit') EditPage() {\n      const {User, Home} = this.constructor;\n\n      if (User.authenticatedUser === undefined) {\n        Home.MainPage.redirect();\n        return null;\n      }\n\n      const fork = useMemo(() => this.fork(), []);\n\n      const save = useAction(async () => {\n        await fork.save();\n        this.merge(fork);\n        this.ItemPage.navigate();\n      }, [fork]);\n\n      return <fork.FormView onSubmit={save} />;\n    }\n\n    @view() FormView({onSubmit}: {onSubmit: Function}) {\n      return (\n        <div>\n          <h3>Article</h3>\n\n          <form\n            onSubmit={async (event) => {\n              event.preventDefault();\n              await onSubmit();\n            }}\n            autoComplete=\"off\"\n            css={{marginTop: '1rem'}}\n          >\n            <Stack direction=\"column\">\n              <Input\n                type=\"text\"\n                value={this.title}\n                onChange={(event) => {\n                  this.title = event.target.value;\n                }}\n                placeholder=\"Title\"\n                required\n              />\n\n              <TextArea\n                rows={5}\n                value={this.description}\n                onChange={(event) => {\n                  this.description = event.target.value;\n                }}\n                placeholder=\"Description\"\n                required\n              />\n\n              <TextArea\n                rows={20}\n                value={this.body}\n                onChange={(event) => {\n                  this.body = event.target.value;\n                }}\n                placeholder=\"Body\"\n                required\n              />\n            </Stack>\n\n            <Button type=\"submit\" color=\"primary\" css={{marginTop: '1.5rem'}}>\n              Publish\n            </Button>\n          </form>\n        </div>\n      );\n    }\n  }\n\n  return Article;\n};\n"
  },
  {
    "path": "website/frontend/src/components/blog.tsx",
    "content": "import {Component, consume} from '@layr/component';\nimport {Routable} from '@layr/routable';\nimport {Fragment, useMemo} from 'react';\nimport {layout, page, useData, useAction} from '@layr/react-integration';\nimport {jsx, useTheme} from '@emotion/react';\n\nimport type {extendUser} from './user';\nimport type {Home} from './home';\nimport type {extendArticle} from './article';\nimport {Title} from '../ui';\n\nexport class Blog extends Routable(Component) {\n  declare ['constructor']: typeof Blog;\n\n  @consume() static User: ReturnType<typeof extendUser>;\n  @consume() static Home: typeof Home;\n  @consume() static Article: ReturnType<typeof extendArticle>;\n\n  @layout('[/]blog') static MainLayout({children}: {children: () => any}) {\n    const theme = useTheme();\n\n    return (\n      <div css={{flexBasis: 650}}>\n        <Title>Blog</Title>\n        <h2 css={{marginBottom: '2rem'}}>\n          <this.MainPage.Link css={{color: theme.colors.text.muted}}>Blog</this.MainPage.Link>\n        </h2>\n        {children()}\n      </div>\n    );\n  }\n\n  @page('[/blog]', {aliases: ['[/blog]/articles']}) static MainPage() {\n    const {Article} = this;\n\n    return useData(\n      async () =>\n        await Article.find(\n          {},\n          {\n            title: true,\n            slug: true,\n            author: {fullName: true, url: true},\n            createdAt: true\n          },\n          {sort: {createdAt: 'desc'}}\n        ),\n\n      (articles) => (\n        <>\n          {articles.map((article, index) => (\n            <div key={article.slug}>\n              {index > 0 && <hr />}\n              <article.ListItemView />\n            </div>\n          ))}\n        </>\n      )\n    );\n  }\n\n  @page('[/blog]/articles/add') static AddArticlePage() {\n    const {User, Home, Article} = this;\n\n    if (User.authenticatedUser === undefined) {\n      Home.MainPage.redirect();\n      return null;\n    }\n\n    const article = useMemo(() => new Article({author: User.authenticatedUser}), []);\n\n    const save = useAction(async () => {\n      await article.save();\n      article.ItemPage.navigate();\n    }, [article]);\n\n    return <article.FormView onSubmit={save} />;\n  }\n}\n"
  },
  {
    "path": "website/frontend/src/components/docs.tsx",
    "content": "import {Component, consume} from '@layr/component';\nimport {stringifyQuery} from '@layr/navigator';\nimport {Routable} from '@layr/routable';\nimport React, {Fragment, useCallback} from 'react';\nimport {jsx, css, useTheme} from '@emotion/react';\nimport {page, view, useData, useNavigator} from '@layr/react-integration';\nimport {Select} from '@emotion-starter/react';\nimport isEqual from 'lodash/isEqual';\n\nimport type {extendApplication} from './application';\nimport docs from '../docs.json';\nimport {useStyles} from '../styles';\nimport {Title} from '../ui';\nimport {Markdown} from '../markdown';\n\nconst VERSIONS = [\n  {name: '1', value: 'v1'},\n  {name: '2', value: 'v2'}\n];\n\nexport const LAST_VERSION = VERSIONS[VERSIONS.length - 1].value;\n\nconst LANGUAGES = [\n  {name: 'JavaScript', value: 'js'},\n  {name: 'TypeScript', value: 'ts'}\n];\n\nconst BASE_URL = '/docs';\n\ntype URLParams = {\n  version: string;\n  bookSlug: string;\n  chapterSlug: string;\n  language: string;\n};\n\ntype Contents = {books: Book[]};\n\ntype Book = {title: string; slug: string; chapters: Chapter[]};\n\ntype Chapter = {\n  title: string;\n  slug: string;\n  file: string;\n  content: string;\n  category: string | undefined;\n  nextChapter?: Chapter;\n};\n\ntype Category = {\n  name: string | undefined;\n  chapters: Chapter[];\n};\n\nexport class Docs extends Routable(Component) {\n  declare ['constructor']: typeof Docs;\n\n  @consume() static Application: ReturnType<typeof extendApplication>;\n\n  @page('[/]docs*') static MainPage() {\n    const {Application} = this;\n\n    const theme = useTheme();\n    const navigator = useNavigator();\n\n    const {version, bookSlug, chapterSlug, language} = this.resolveURL();\n\n    if (\n      version === undefined ||\n      bookSlug === undefined ||\n      chapterSlug === undefined ||\n      language === undefined\n    ) {\n      return <Application.NotFoundView />;\n    }\n\n    if (\n      navigator.getCurrentPath() !== this.generatePath({version, bookSlug, chapterSlug}) ||\n      !isEqual(navigator.getCurrentQuery(), this.generateQuery({language}))\n    ) {\n      navigator.redirect(\n        this.generateURL({\n          version,\n          bookSlug,\n          chapterSlug,\n          language,\n          hash: navigator.getCurrentHash()\n        })\n      );\n      return null;\n    }\n\n    return (\n      <div\n        css={theme.responsive({\n          flexBasis: 960,\n          display: 'flex',\n          flexWrap: ['nowrap', , 'wrap-reverse']\n        })}\n      >\n        <div css={theme.responsive({width: ['250px', , '100%'], paddingRight: [20, , 0]})}>\n          <this.ContentsView\n            version={version}\n            bookSlug={bookSlug}\n            chapterSlug={chapterSlug}\n            language={language}\n          />\n        </div>\n\n        <hr css={theme.responsive({display: ['none', , 'block'], width: '100%'})} />\n\n        <div css={{flexGrow: 1, flexBasis: 640}}>\n          <this.ChapterView\n            version={version}\n            bookSlug={bookSlug}\n            chapterSlug={chapterSlug}\n            language={language}\n          />\n        </div>\n      </div>\n    );\n  }\n\n  @view() static ContentsView({version, bookSlug, chapterSlug, language}: URLParams) {\n    const theme = useTheme();\n    const styles = useStyles();\n    const navigator = useNavigator();\n\n    const contents = this.getContents({version});\n\n    const bookMenuStyle = css({...styles.unstyledList, margin: 0});\n    const bookMenuItemStyle = css({marginTop: '1rem'});\n    const bookMenuItemTitleStyle = css({\n      marginBottom: '-.25rem',\n      fontSize: theme.fontSizes.h5,\n      fontWeight: theme.fontWeights.semibold,\n      color: theme.colors.text.muted\n    });\n\n    const categoryMenuStyle = css({...styles.unstyledList, margin: 0});\n    const categoryMenuItemStyle = css({marginTop: '.75rem'});\n    const categoryMenuItemTitleStyle = css({\n      fontSize: theme.fontSizes.small,\n      fontWeight: theme.fontWeights.semibold,\n      textTransform: 'uppercase',\n      letterSpacing: 1\n    });\n\n    const chapterMenuStyle = css({...styles.unstyledList, margin: 0, marginTop: '.2rem'});\n    const chapterMenuItemStyle = css({\n      marginTop: 0,\n      whiteSpace: 'nowrap',\n      overflow: 'hidden',\n      textOverflow: 'ellipsis',\n      color: theme.colors.primary.normal // Fixes the color of the ellipsis\n    });\n\n    return (\n      <nav>\n        <div css={{marginBottom: 20}}>\n          <this.OptionsView\n            version={version}\n            bookSlug={bookSlug}\n            chapterSlug={chapterSlug}\n            language={language}\n          />\n        </div>\n\n        <ul css={bookMenuStyle}>\n          {contents.books.map((book) => {\n            const categories = getBookCategories(book);\n\n            return (\n              <li key={book.slug} css={bookMenuItemStyle}>\n                <div css={bookMenuItemTitleStyle}>{book.title}</div>\n                <ul css={categoryMenuStyle}>\n                  {categories.map((category) => {\n                    return (\n                      <li key={category.name || 'uncategorized'} css={categoryMenuItemStyle}>\n                        {category.name && (\n                          <div css={categoryMenuItemTitleStyle}>{category.name}</div>\n                        )}\n                        <ul css={chapterMenuStyle}>\n                          {category.chapters.map((chapter) => {\n                            return (\n                              <li key={chapter.slug} css={chapterMenuItemStyle}>\n                                <navigator.Link\n                                  to={this.generateURL({\n                                    version,\n                                    bookSlug: book.slug,\n                                    chapterSlug: chapter.slug,\n                                    language\n                                  })}\n                                  activeStyle={{color: theme.colors.primary.highlighted}}\n                                >\n                                  {chapter.title}\n                                </navigator.Link>\n                              </li>\n                            );\n                          })}\n                        </ul>\n                      </li>\n                    );\n                  })}\n                </ul>\n              </li>\n            );\n          })}\n        </ul>\n      </nav>\n    );\n  }\n\n  @view() static OptionsView({version, bookSlug, chapterSlug, language}: URLParams) {\n    const theme = useTheme();\n    const navigator = useNavigator();\n\n    const hash = navigator.getCurrentHash();\n\n    const changeVersion = useCallback(\n      (event: React.ChangeEvent<HTMLSelectElement>) => {\n        const version = event.target.value;\n        navigator.redirect(this.generateURL({version, language}));\n      },\n      [language]\n    );\n\n    const changeLanguage = useCallback(\n      (event: React.ChangeEvent<HTMLSelectElement>) => {\n        const language = event.target.value;\n        window.localStorage.setItem('language', language);\n        navigator.redirect(this.generateURL({version, bookSlug, chapterSlug, language, hash}));\n      },\n      [version, bookSlug, chapterSlug, hash]\n    );\n\n    const labelStyle = css({\n      marginBottom: 3,\n      color: theme.colors.text.muted,\n      fontSize: theme.fontSizes.small\n    });\n\n    return (\n      <div css={{display: 'flex'}}>\n        <div css={{display: 'flex', flexDirection: 'column', marginRight: 15}}>\n          <label htmlFor=\"version-selector\" css={labelStyle}>\n            Version\n          </label>\n          <Select id=\"version-selector\" value={version} onChange={changeVersion} size=\"small\">\n            {[...VERSIONS].reverse().map(({name, value}) => (\n              <option key={value} value={value}>\n                {name}\n              </option>\n            ))}\n          </Select>\n        </div>\n\n        <div css={{display: 'flex', flexDirection: 'column'}}>\n          <label htmlFor=\"version-selector\" css={labelStyle}>\n            Language\n          </label>\n          <Select id=\"version-selector\" value={language} onChange={changeLanguage} size=\"small\">\n            {LANGUAGES.map(({name, value}) => (\n              <option key={value} value={value}>\n                {name}\n              </option>\n            ))}\n          </Select>\n        </div>\n      </div>\n    );\n  }\n\n  @view() static ChapterView({version, bookSlug, chapterSlug, language}: URLParams) {\n    const theme = useTheme();\n    const navigator = useNavigator();\n\n    return useData(\n      async () => await this.getChapter({version, bookSlug, chapterSlug}),\n\n      (chapter) => {\n        const {nextChapter} = chapter;\n\n        return (\n          <>\n            <Title>{chapter.title}</Title>\n\n            <Markdown languageFilter={language}>{chapter.content}</Markdown>\n\n            {nextChapter && (\n              <div css={{marginBottom: 15}}>\n                <hr />\n                <div>\n                  <span style={{color: theme.colors.text.muted}}>Next:</span>{' '}\n                  <navigator.Link\n                    to={this.generateURL({\n                      version,\n                      bookSlug,\n                      chapterSlug: nextChapter.slug,\n                      language\n                    })}\n                  >\n                    {nextChapter.title} →\n                  </navigator.Link>\n                </div>\n              </div>\n            )}\n          </>\n        );\n      },\n\n      [bookSlug, chapterSlug]\n    );\n  }\n\n  static _contents: Record<string, Contents> = {};\n\n  static getContents({version}: {version: string}) {\n    if (this._contents[version] === undefined) {\n      const contents: Contents = (docs as any).versions[version];\n\n      for (const book of contents.books) {\n        let previousChapter: Chapter | undefined;\n\n        for (const chapter of book.chapters) {\n          if (previousChapter !== undefined) {\n            previousChapter.nextChapter = chapter;\n          }\n\n          previousChapter = chapter;\n        }\n      }\n\n      this._contents[version] = contents;\n    }\n\n    return this._contents[version];\n  }\n\n  static async getChapter({\n    version,\n    bookSlug,\n    chapterSlug\n  }: {\n    version: string;\n    bookSlug: string;\n    chapterSlug: string;\n  }) {\n    const contents = this.getContents({version});\n\n    const book = contents.books.find((book) => book.slug === bookSlug);\n\n    if (book === undefined) {\n      throw new Error(`Book not found (path: '${bookSlug}/${chapterSlug}')`);\n    }\n\n    const chapter = book.chapters.find((chapter) => chapter.slug === chapterSlug);\n\n    if (chapter === undefined) {\n      throw new Error(`Chapter not found (path: '${bookSlug}/${chapterSlug}')`);\n    }\n\n    if (chapter.content === undefined) {\n      try {\n        const url = `${BASE_URL}/${version}/${chapter.file}`;\n        const response = await fetch(url);\n        const content = await response.text();\n\n        if (response.status !== 200) {\n          throw new Error('An error occurred while fetching the chapter content');\n        }\n\n        chapter.content = content;\n      } catch (error: any) {\n        error.displayMessage =\n          'Sorry, something went wrong while loading the documentation chapter.';\n        throw error;\n      }\n    }\n\n    return chapter;\n  }\n\n  static resolveURL() {\n    const navigator = this.getNavigator();\n\n    const path = navigator.getCurrentPath();\n    const query = navigator.getCurrentQuery();\n\n    const {version, bookSlug, chapterSlug} = this.resolvePath(path);\n    const {language} = this.resolveQuery(query);\n\n    return {version, bookSlug, chapterSlug, language};\n  }\n\n  static resolvePath(path: string) {\n    const result: {version?: string; bookSlug?: string; chapterSlug?: string} = {};\n\n    let [version, bookSlug, chapterSlug, ...otherSegments] = path.slice('/docs/'.length).split('/');\n\n    if (otherSegments.length > 0) {\n      return result;\n    }\n\n    result.version = this.resolveVersion(version);\n\n    if (result.version === undefined) {\n      return result;\n    }\n\n    const contents = this.getContents({version: result.version});\n\n    if (bookSlug === undefined || bookSlug === '') {\n      bookSlug = contents.books[0].slug;\n    }\n\n    const book = contents.books.find((book) => book.slug === bookSlug);\n\n    if (book === undefined) {\n      return result;\n    }\n\n    result.bookSlug = book.slug;\n\n    if (chapterSlug === undefined || chapterSlug === '') {\n      chapterSlug = book.chapters[0].slug;\n    }\n\n    const chapter = book.chapters.find((chapter) => chapter.slug === chapterSlug);\n\n    if (chapter === undefined) {\n      return result;\n    }\n\n    result.chapterSlug = chapter.slug;\n\n    return result;\n  }\n\n  static resolveVersion(version: string | undefined) {\n    if (version === undefined || version === '') {\n      version = LAST_VERSION;\n    }\n\n    if (VERSIONS.find(({value}) => value === version) === undefined) {\n      return undefined;\n    }\n\n    return version;\n  }\n\n  static resolveQuery(query: {language?: string}) {\n    const language = this.resolveLanguage(query.language);\n\n    return {language};\n  }\n\n  static resolveLanguage(language: string | undefined) {\n    if (language === undefined) {\n      language = window.localStorage.getItem('language') || LANGUAGES[0].value;\n    }\n\n    if (LANGUAGES.find(({value}) => value === language) === undefined) {\n      return undefined;\n    }\n\n    return language;\n  }\n\n  static generateURL({\n    version,\n    bookSlug,\n    chapterSlug,\n    language,\n    hash\n  }: {\n    version: string;\n    bookSlug?: string;\n    chapterSlug?: string;\n    language: string;\n    hash?: string;\n  }) {\n    let url = this.generatePath({version, bookSlug, chapterSlug});\n\n    const queryString = stringifyQuery(this.generateQuery({language}));\n\n    if (queryString !== '') {\n      url += `?${queryString}`;\n    }\n\n    if (hash !== undefined) {\n      url += `#${hash}`;\n    }\n\n    return url;\n  }\n\n  static generatePath({\n    version,\n    bookSlug,\n    chapterSlug\n  }: {\n    version: string;\n    bookSlug?: string;\n    chapterSlug?: string;\n  }) {\n    let path = `/docs/${version}`;\n\n    if (bookSlug !== undefined) {\n      path += `/${bookSlug}`;\n    }\n\n    if (chapterSlug !== undefined) {\n      path += `/${chapterSlug}`;\n    }\n\n    return path;\n  }\n\n  static generateQuery({language}: {language: string}) {\n    return {language};\n  }\n}\n\nfunction getBookCategories(book: Book) {\n  const categories: Category[] = [];\n\n  for (const chapter of book.chapters) {\n    if (categories.length === 0 || categories[categories.length - 1].name !== chapter.category) {\n      categories.push({name: chapter.category, chapters: []});\n    }\n\n    categories[categories.length - 1].chapters.push(chapter);\n  }\n\n  return categories;\n}\n"
  },
  {
    "path": "website/frontend/src/components/home.tsx",
    "content": "import {Component, consume} from '@layr/component';\nimport {Routable} from '@layr/routable';\nimport {Fragment} from 'react';\nimport {jsx, useTheme} from '@emotion/react';\nimport {Button} from '@emotion-starter/react';\nimport {Container} from '@emotion-kit/react';\nimport {page, view} from '@layr/react-integration';\n\nimport type {extendApplication} from './application';\nimport type {Docs} from './docs';\nimport type {extendNewsletter} from './newsletter';\nimport {Markdown} from '../markdown';\nimport {FeatureSection, FullHeight, Title} from '../ui';\nimport typicalVsLayr from '../assets/typical-stack-vs-layr-stack.png';\n\nconst NO_WEB_API_BACKEND_EXAMPLE = `\n\\`\\`\\`\nimport {Component, attribute, method, expose} from '@layr/component';\nimport {ComponentHTTPServer} from '@layr/component-http-server';\n\nclass Greeter extends Component {\n  @expose({set: true}) @attribute() name = 'World';\n\n  @expose({call: true}) @method() async hello() {\n    return \\`Hello, \\${this.name}!\\`;\n  }\n}\n\nconst server = new ComponentHTTPServer(Greeter, {port: 3210});\n\nserver.start();\n\\`\\`\\`\n`;\n\nconst NO_WEB_API_FRONTEND_EXAMPLE = `\n\\`\\`\\`\nimport {ComponentHTTPClient} from '@layr/component-http-client';\n\nconst client = new ComponentHTTPClient('http://localhost:3210');\n\nconst BackendGreeter = await client.getComponent();\n\nclass Greeter extends BackendGreeter {\n  async hello() {\n    return (await super.hello()).toUpperCase();\n  }\n}\n\nconst greeter = new Greeter({name: 'Steve'});\n\nawait greeter.hello(); // => 'HELLO, STEVE!'\n\\`\\`\\`\n`;\n\nconst ORM_DATA_MODELING_EXAMPLE = `\n\\`\\`\\`\nimport {Component} from '@layr/component';\nimport {Storable, primaryIdentifier, attribute} from '@layr/storable';\n\nclass Movie extends Storable(Component) {\n  @primaryIdentifier() id;\n  @attribute() title;\n  @attribute() country;\n  @attribute() year;\n}\n\\`\\`\\`\n`;\n\nconst ORM_STORE_REGISTRATION_EXAMPLE = `\n\\`\\`\\`\nimport {MongoDBStore} from '@layr/mongodb-store';\n\nconst store = new MongoDBStore('mongodb://user:pass@host:port/db');\n\nstore.registerStorable(Movie);\n\\`\\`\\`\n`;\n\nconst ORM_STORE_CRUD_OPERATIONS_EXAMPLE = `\n\\`\\`\\`\n// Create\nconst movie = new Movie({\n  id: 'abc123',\n  title: 'Inception',\n  country: 'USA',\n  year: 2010\n});\nawait movie.save();\n\n// Read\nconst movie = await Movie.get('abc123');\n\n// Update\nmovie.title = 'Inception 2';\nawait movie.save();\n\n// Delete\nawait movie.delete();\n\\`\\`\\`\n`;\n\nconst ORM_STORE_FINDING_DATA_EXAMPLE = `\n\\`\\`\\`\n// Find the movies starting with an 'I'\nconst movies = await Movie.find({title: {$startsWith: 'I'}});\n\n// Find the movies released after 2010\nconst movies = await Movie.find({year: {$greaterThan: 2010}});\n\n// Find the Japanese movies released in 2010\nconst movies = await Movie.find({country: 'Japan', year: 2010});\n\\`\\`\\`\n`;\n\nconst ENCAPSULATED_USER_INTERFACE_EXAMPLE = `\n\\`\\`\\`\nimport {Component, primaryIdentifier, attribute} from '@layr/component';\nimport {Routable} from '@layr/routable';\nimport React from 'react';\nimport {layout, page, view} from '@layr/react-integration';\n\nclass Movie extends Routable(Component) {\n  @primaryIdentifier() id;\n  @attribute() title;\n  @attribute() year;\n  @attribute() country;\n\n  @layout('/movies') static Layout({children}) {\n    return (\n      <>\n        <h1>Movies</h1>\n        {children()}\n      </>\n    );\n  }\n\n  @page('[/movies]/:id') Page() {\n    return (\n      <>\n        <this.Heading />\n        <this.Details />\n      </>\n    );\n  }\n\n  @view() Heading() {\n    return (\n      <h2>\n        {this.title} ({this.year})\n      </h2>\n    );\n  }\n\n  @view() Details() {\n    return <p>Country: {this.country}</p>;\n  }\n}\n\\`\\`\\`\n`;\n\nexport class Home extends Routable(Component) {\n  @consume() static Application: ReturnType<typeof extendApplication>;\n  @consume() static Docs: typeof Docs;\n  @consume() static Newsletter: ReturnType<typeof extendNewsletter>;\n\n  @page('[]/', {aliases: ['[]/index.html']}) static MainPage() {\n    const {Application, Newsletter} = this;\n\n    const theme = useTheme();\n\n    return (\n      <>\n        <Title>Dramatically simplify full‑stack development</Title>\n\n        <FullHeight\n          css={{\n            display: 'flex',\n            flexDirection: 'column',\n            backgroundColor: theme.colors.background.highlighted\n          }}\n        >\n          <div>\n            <Container>\n              <Application.HeaderView />\n            </Container>\n          </div>\n          <this.HeroView css={{flexGrow: 1}} />\n          <this.ScrollerView id=\"features\" />\n        </FullHeight>\n\n        <div id=\"features\" css={{maxWidth: 850, margin: '0 auto'}}>\n          <this.NoWebAPIView />\n          <hr css={{margin: 0}} />\n          <this.ORMView />\n          <hr css={{margin: 0}} />\n          <this.UserInterfaceView />\n          <hr css={{margin: 0}} />\n          <this.FindOutMoreView />\n          <hr css={{margin: 0}} />\n          <Newsletter.SubscriptionView />\n        </div>\n\n        <div css={{backgroundColor: theme.colors.background.highlighted}}>\n          <Container>\n            <Application.FooterView />\n          </Container>\n        </div>\n      </>\n    );\n  }\n\n  @view() static HeroView({...props}) {\n    const theme = useTheme();\n\n    return (\n      <div\n        css={theme.responsive({\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          padding: ['3rem 1.5rem', , '3rem 15px']\n        })}\n        {...props}\n      >\n        <div\n          css={theme.responsive({\n            display: 'flex',\n            flexDirection: ['row', 'column-reverse'],\n            alignItems: 'center',\n            maxWidth: '1024px'\n          })}\n        >\n          <div css={theme.responsive({marginTop: [0, '3rem'], textAlign: ['left', 'center']})}>\n            <h2\n              css={theme.responsive({\n                fontSize: [, , '1.953rem', '1.563rem'],\n                lineHeight: '1.45'\n              })}\n            >\n              Dramatically Simplify Full‑Stack Development\n            </h2>\n            <div\n              css={theme.responsive({\n                fontSize: ['1.563rem', , , '1.25rem'],\n                color: theme.colors.text.muted\n              })}\n            >\n              Inherit your frontend from your backend and build a highly dynamic app as if it were\n              made of a single layer.\n            </div>\n            <Button\n              onClick={() => {\n                this.Docs.MainPage.navigate();\n              }}\n              size=\"large\"\n              color=\"secondary\"\n              css={{marginTop: '2rem'}}\n            >\n              Read the Docs\n            </Button>\n          </div>\n\n          <img\n            src={typicalVsLayr}\n            alt=\"Typical stack vs Layr stack\"\n            css={theme.responsive({marginLeft: ['2.5rem', 0], maxWidth: [500, , '100%']})}\n          />\n        </div>\n      </div>\n    );\n  }\n\n  @view() static ScrollerView({id}: {id: string}) {\n    const theme = useTheme();\n\n    return (\n      <div css={{alignSelf: 'center', marginBottom: '1.75rem', lineHeight: 1}}>\n        <a\n          href={`#${id}`}\n          onClick={(event) => {\n            const element = document.getElementById(id);\n            if (element) {\n              event.preventDefault();\n              element.scrollIntoView({block: 'start', behavior: 'smooth'});\n            }\n          }}\n          css={{\n            'color': theme.colors.text.muted,\n            ':hover': {color: theme.colors.text.normal, textDecoration: 'none'}\n          }}\n        >\n          ▼\n        </a>\n      </div>\n    );\n  }\n\n  @view() static NoWebAPIView() {\n    return (\n      <FeatureSection\n        title=\"Look Ma, No Web API\"\n        description=\"Stop wasting your time building a web API. With Layr, the frontend and the backend can [communicate directly](https://dev.to/mvila/good-bye-web-apis-2bel) as if they were not separated.\"\n      >\n        <div css={{marginTop: '3rem', maxWidth: 640}}>\n          <h5 css={{marginTop: '2rem'}}>Backend</h5>\n          <Markdown>{NO_WEB_API_BACKEND_EXAMPLE}</Markdown>\n          <h5 css={{marginTop: '2rem'}}>Frontend</h5>\n          <Markdown>{NO_WEB_API_FRONTEND_EXAMPLE}</Markdown>\n        </div>\n      </FeatureSection>\n    );\n  }\n\n  @view() static ORMView() {\n    return (\n      <FeatureSection\n        title=\"Abstracted Away Database\"\n        description={\n          \"Extend your classes with the [`Storable()`](/docs/v2/reference/storable) mixin, register them into a [store](/docs/v2/reference/store), and you're ready to build an app without having to worry about the database.\"\n        }\n      >\n        <div css={{marginTop: '3rem', maxWidth: 640}}>\n          <h5 css={{marginTop: '2rem'}}>Data Modeling</h5>\n          <Markdown>{ORM_DATA_MODELING_EXAMPLE}</Markdown>\n          <h5 css={{marginTop: '2rem'}}>Store Registration</h5>\n          <Markdown>{ORM_STORE_REGISTRATION_EXAMPLE}</Markdown>\n          <h5 css={{marginTop: '2rem'}}>CRUD Operations</h5>\n          <Markdown>{ORM_STORE_CRUD_OPERATIONS_EXAMPLE}</Markdown>\n          <h5 css={{marginTop: '2rem'}}>Finding Data</h5>\n          <Markdown>{ORM_STORE_FINDING_DATA_EXAMPLE}</Markdown>\n        </div>\n      </FeatureSection>\n    );\n  }\n\n  @view() static UserInterfaceView() {\n    return (\n      <FeatureSection\n        title=\"Encapsulated User Interface\"\n        description={\n          'Implement your [layouts](/docs/v2/reference/react-integration#layout-decorator), [pages](/docs/v2/reference/react-integration#page-decorator), and [views](/docs/v2/reference/react-integration#view-decorator) as methods of your models, and keep your app as cohesive as possible.'\n        }\n      >\n        <div css={{marginTop: '2.25rem', maxWidth: 640}}>\n          <Markdown>{ENCAPSULATED_USER_INTERFACE_EXAMPLE}</Markdown>\n        </div>\n      </FeatureSection>\n    );\n  }\n\n  @view() static FindOutMoreView() {\n    return (\n      <FeatureSection\n        title=\"Find Out More\"\n        description={\n          'Layr provides everything you need to build a highly dynamic app from start to finish. Find out more by checking out the documentation.'\n        }\n      >\n        <Button\n          onClick={() => {\n            this.Docs.MainPage.navigate();\n          }}\n          size=\"large\"\n          color=\"secondary\"\n          css={{marginTop: '2rem', marginBottom: '.5rem'}}\n        >\n          Read the Docs\n        </Button>\n      </FeatureSection>\n    );\n  }\n}\n"
  },
  {
    "path": "website/frontend/src/components/newsletter.tsx",
    "content": "import {useState} from 'react';\nimport {view, useAction} from '@layr/react-integration';\nimport {jsx, useTheme} from '@emotion/react';\nimport {Input, Button} from '@emotion-starter/react';\n\nimport type {Newsletter as BackendNewsletter} from '../../../backend/src/components/newsletter';\nimport {FeatureSection} from '../ui';\n\nexport const extendNewsletter = (Base: typeof BackendNewsletter) => {\n  class Newsletter extends Base {\n    @view() static SubscriptionView() {\n      const theme = useTheme();\n\n      const [email, setEmail] = useState('');\n      const [isSubscribed, setIsSubscribed] = useState(false);\n\n      const subscribe = useAction(async () => {\n        await this.subscribe({email});\n        setIsSubscribed(true);\n      }, [email]);\n\n      return (\n        <FeatureSection\n          title={!isSubscribed ? 'Stay Updated' : 'Thank You!'}\n          description={\n            !isSubscribed ? \"Keep up on all that's happening with Layr!\" : \"We'll keep you updated.\"\n          }\n        >\n          {!isSubscribed && (\n            <form\n              onSubmit={(event) => {\n                event.preventDefault();\n                subscribe();\n              }}\n              css={{\n                display: 'flex',\n                flexWrap: 'wrap',\n                justifyContent: 'center',\n                alignItems: 'center',\n                marginTop: '1.75rem',\n                marginBottom: '.5rem'\n              }}\n            >\n              <Input\n                type=\"email\"\n                onChange={(event) => {\n                  setEmail(event.target.value);\n                }}\n                value={email}\n                required\n                placeholder=\"Your email address\"\n                size=\"large\"\n                css={theme.responsive({\n                  width: [300, , '100%'],\n                  marginRight: ['0.75rem', , 0],\n                  marginBottom: [0, , '0.75rem']\n                })}\n              />\n              <Button\n                type=\"submit\"\n                size=\"large\"\n                color=\"primary\"\n                css={theme.responsive({width: ['auto', , '100%']})}\n              >\n                Subscribe\n              </Button>\n            </form>\n          )}\n        </FeatureSection>\n      );\n    }\n  }\n\n  return Newsletter;\n};\n"
  },
  {
    "path": "website/frontend/src/components/user.tsx",
    "content": "import {consume} from '@layr/component';\nimport {attribute} from '@layr/storable';\nimport {Routable} from '@layr/routable';\nimport {page, view, useData, useAction} from '@layr/react-integration';\nimport {useState, useMemo} from 'react';\nimport {jsx} from '@emotion/react';\nimport {Input, Button} from '@emotion-starter/react';\nimport {Stack} from '@emotion-kit/react';\n\nimport type {User as BackendUser} from '../../../backend/src/components/user';\nimport type {Home} from './home';\nimport {Title} from '../ui';\n\nexport const extendUser = (Base: typeof BackendUser) => {\n  class User extends Routable(Base) {\n    declare ['constructor']: typeof User;\n\n    @consume() static Home: typeof Home;\n\n    @attribute('string?', {\n      getter() {\n        return window.localStorage.getItem('token') || undefined;\n      },\n      setter(token) {\n        if (token !== undefined) {\n          window.localStorage.setItem('token', token);\n        } else {\n          window.localStorage.removeItem('token');\n        }\n      }\n    })\n    static token?: string;\n\n    static authenticatedUser?: User;\n\n    static async initializer() {\n      this.authenticatedUser = (await this.getAuthenticatedUser()) as User;\n    }\n\n    @page('[/]sign-up') static SignUpPage() {\n      const {Home} = this;\n\n      if (this.authenticatedUser !== undefined) {\n        Home.MainPage.redirect();\n        return null;\n      }\n\n      const user = useMemo(() => new this(), []);\n\n      return <user.SignUpView />;\n    }\n\n    @view() SignUpView() {\n      const {Home} = this.constructor;\n\n      const [inviteToken, setInviteToken] = useState('');\n\n      const signUp = useAction(async () => {\n        await this.signUp({inviteToken});\n        Home.MainPage.reload();\n      }, [inviteToken]);\n\n      return (\n        <div css={{flexBasis: 400}}>\n          <Title>Sign up</Title>\n\n          <h2>Sign Up</h2>\n\n          <form\n            onSubmit={(event) => {\n              event.preventDefault();\n              signUp();\n            }}\n            css={{marginTop: '2rem'}}\n          >\n            <Stack direction=\"column\">\n              <Input\n                type=\"email\"\n                value={this.email}\n                onChange={(event) => {\n                  this.email = event.target.value;\n                }}\n                placeholder=\"Email\"\n                required\n                size=\"large\"\n              />\n\n              <Input\n                type=\"password\"\n                value={this.password}\n                onChange={(event) => {\n                  this.password = event.target.value;\n                }}\n                placeholder=\"Password\"\n                autoComplete=\"new-password\"\n                required\n                size=\"large\"\n              />\n\n              <Input\n                type=\"text\"\n                value={this.firstName}\n                onChange={(event) => {\n                  this.firstName = event.target.value;\n                }}\n                placeholder=\"First name\"\n                required\n                size=\"large\"\n              />\n\n              <Input\n                type=\"text\"\n                value={this.lastName}\n                onChange={(event) => {\n                  this.lastName = event.target.value;\n                }}\n                placeholder=\"Last name\"\n                required\n                size=\"large\"\n              />\n\n              <Input\n                type=\"url\"\n                value={this.url}\n                onChange={(event) => {\n                  this.url = event.target.value;\n                }}\n                placeholder=\"URL\"\n                required\n                size=\"large\"\n              />\n\n              <Input\n                type=\"text\"\n                value={inviteToken}\n                onChange={(event) => {\n                  setInviteToken(event.target.value);\n                }}\n                placeholder=\"Invite token\"\n                size=\"large\"\n              />\n            </Stack>\n\n            <Button type=\"submit\" size=\"large\" color=\"primary\" css={{marginTop: '2rem'}}>\n              Sign up\n            </Button>\n          </form>\n        </div>\n      );\n    }\n\n    @page('[/]sign-in') static SignInPage() {\n      const {Home} = this;\n\n      if (this.authenticatedUser !== undefined) {\n        Home.MainPage.redirect();\n        return null;\n      }\n\n      const user = useMemo(() => new this(), []);\n\n      return <user.SignInView />;\n    }\n\n    @view() SignInView() {\n      const {Home} = this.constructor;\n\n      const signIn = useAction(async () => {\n        await this.signIn();\n        Home.MainPage.reload();\n      });\n\n      return (\n        <div css={{flexBasis: 400}}>\n          <Title>Sign in</Title>\n\n          <h2>Sign In</h2>\n\n          <form\n            onSubmit={(event) => {\n              event.preventDefault();\n              signIn();\n            }}\n            css={{marginTop: '2rem'}}\n          >\n            <Stack direction=\"column\">\n              <Input\n                type=\"email\"\n                value={this.email}\n                onChange={(event) => {\n                  this.email = event.target.value;\n                }}\n                placeholder=\"Email\"\n                required\n                size=\"large\"\n              />\n\n              <Input\n                type=\"password\"\n                value={this.password}\n                onChange={(event) => {\n                  this.password = event.target.value;\n                }}\n                placeholder=\"Password\"\n                required\n                size=\"large\"\n              />\n            </Stack>\n\n            <Button type=\"submit\" size=\"large\" color=\"primary\" css={{marginTop: '2rem'}}>\n              Sign in\n            </Button>\n          </form>\n        </div>\n      );\n    }\n\n    @page('/sign-out') static signOutPage() {\n      const {Home} = this;\n\n      this.token = undefined;\n      Home.MainPage.reload();\n\n      return null;\n    }\n\n    @page('[/]invite') static InvitePage() {\n      return useData(\n        async () => {\n          return await this.generateInviteToken();\n        },\n\n        (inviteToken) => (\n          <div>\n            <p>Invite token:</p>\n            <pre>{inviteToken}</pre>\n          </div>\n        )\n      );\n    }\n  }\n\n  return User;\n};\n"
  },
  {
    "path": "website/frontend/src/custom.d.ts",
    "content": "declare module '*.png';\ndeclare module '*.jpg';\ndeclare module '*.jpeg';\ndeclare module '*.webp';\ndeclare module '*.gif';\ndeclare module '*.svg';\n"
  },
  {
    "path": "website/frontend/src/docs.json",
    "content": "{\n  \"versions\": {\n    \"v1\": {\n      \"books\": [\n        {\n          \"title\": \"Getting Started\",\n          \"slug\": \"introduction\",\n          \"chapters\": [\n            {\n              \"title\": \"Introduction\",\n              \"slug\": \"introduction\",\n              \"file\": \"introduction/introduction-1NNtkbXN0zHvG6VR0jvPRK-edited.immutable.md\"\n            },\n            {\n              \"title\": \"Hello, World!\",\n              \"slug\": \"hello-world\",\n              \"file\": \"introduction/hello-world-7F76XnjXXBB7eeZTpEZwEX-edited.immutable.md\"\n            },\n            {\n              \"title\": \"Storing Data\",\n              \"slug\": \"data-storage\",\n              \"file\": \"introduction/data-storage-1YDg3LGZPF09H3opJUEDSp-edited.immutable.md\"\n            },\n            {\n              \"title\": \"Building a Web App\",\n              \"slug\": \"web-app\",\n              \"file\": \"introduction/web-app-5okryUyuriFFHXtFK9uZNY-edited.immutable.md\"\n            },\n            {\n              \"title\": \"Bringing Some Routes\",\n              \"slug\": \"routing\",\n              \"file\": \"introduction/routing-4ubaWloMHuNNs0foagYUwi-edited.immutable.md\"\n            },\n            {\n              \"title\": \"Handling Authorization\",\n              \"slug\": \"authorization\",\n              \"file\": \"introduction/authorization-6eooNLZnaLZk4eJNHNGwUp-edited.immutable.md\"\n            }\n          ]\n        },\n        {\n          \"title\": \"Reference\",\n          \"slug\": \"reference\",\n          \"chapters\": [\n            {\n              \"title\": \"Component\",\n              \"slug\": \"component\",\n              \"file\": \"reference/component-6LvsJL2MA9RN6hmT0bBacd.immutable.md\",\n              \"category\": \"Basics\"\n            },\n            {\n              \"title\": \"EmbeddedComponent\",\n              \"slug\": \"embedded-component\",\n              \"file\": \"reference/embedded-component-4MiZBzspJQbUshU8Q93aJ8.immutable.md\",\n              \"category\": \"Basics\"\n            },\n            {\n              \"title\": \"Property\",\n              \"slug\": \"property\",\n              \"file\": \"reference/property-5bbULbxC52tIPaMAcPEp27.immutable.md\",\n              \"category\": \"Basics\"\n            },\n            {\n              \"title\": \"Attribute\",\n              \"slug\": \"attribute\",\n              \"file\": \"reference/attribute-4P9dTivZ9HJ4Flr4Y8cTKv.immutable.md\",\n              \"category\": \"Basics\"\n            },\n            {\n              \"title\": \"IdentifierAttribute\",\n              \"slug\": \"identifier-attribute\",\n              \"file\": \"reference/identifier-attribute-6Jgrzmrlv4QJoBZVTYYDb9.immutable.md\",\n              \"category\": \"Basics\"\n            },\n            {\n              \"title\": \"PrimaryIdentifierAttribute\",\n              \"slug\": \"primary-identifier-attribute\",\n              \"file\": \"reference/primary-identifier-attribute-6qTkOuHNN4Bq3EjC9JGVqr.immutable.md\",\n              \"category\": \"Basics\"\n            },\n            {\n              \"title\": \"SecondaryIdentifierAttribute\",\n              \"slug\": \"secondary-identifier-attribute\",\n              \"file\": \"reference/secondary-identifier-attribute-7BqK9EmnCMCHIAroYiQDth.immutable.md\",\n              \"category\": \"Basics\"\n            },\n            {\n              \"title\": \"Method\",\n              \"slug\": \"method\",\n              \"file\": \"reference/method-3mc2TakviGDKFcdpxFaCIu.immutable.md\",\n              \"category\": \"Basics\"\n            },\n            {\n              \"title\": \"ValueType\",\n              \"slug\": \"value-type\",\n              \"file\": \"reference/value-type-23f3WjnZNDfFejs5ceJVcY.immutable.md\",\n              \"category\": \"Basics\"\n            },\n            {\n              \"title\": \"Validator\",\n              \"slug\": \"validator\",\n              \"file\": \"reference/validator-2strlvnapeTZmbWT3o3VM1.immutable.md\",\n              \"category\": \"Basics\"\n            },\n            {\n              \"title\": \"AttributeSelector\",\n              \"slug\": \"attribute-selector\",\n              \"file\": \"reference/attribute-selector-7FZWQpwLR7jpUHoBpvO9Et.immutable.md\",\n              \"category\": \"Basics\"\n            },\n            {\n              \"title\": \"IdentityMap\",\n              \"slug\": \"identity-map\",\n              \"file\": \"reference/identity-map-01fsjrVv6cSC6awmnNnn6c.immutable.md\",\n              \"category\": \"Basics\"\n            },\n            {\n              \"title\": \"ComponentClient\",\n              \"slug\": \"component-client\",\n              \"file\": \"reference/component-client-1ISSjJNRL12I33addutRh4.immutable.md\",\n              \"category\": \"Communication\"\n            },\n            {\n              \"title\": \"ComponentServer\",\n              \"slug\": \"component-server\",\n              \"file\": \"reference/component-server-e5RUuQXVcCIVLxHiNzCDO.immutable.md\",\n              \"category\": \"Communication\"\n            },\n            {\n              \"title\": \"ComponentHTTPClient\",\n              \"slug\": \"component-http-client\",\n              \"file\": \"reference/component-http-client-6g8M9kRcBS4AQwFtAp7t9z.immutable.md\",\n              \"category\": \"Communication\"\n            },\n            {\n              \"title\": \"ComponentHTTPServer\",\n              \"slug\": \"component-http-server\",\n              \"file\": \"reference/component-http-server-5VDR5fS2uD9iTkuHRSywjz.immutable.md\",\n              \"category\": \"Communication\"\n            },\n            {\n              \"title\": \"component-express-middleware\",\n              \"slug\": \"component-express-middleware\",\n              \"file\": \"reference/component-express-middleware-6UieWJkEvLdXLYug4xuM16.immutable.md\",\n              \"category\": \"Communication\"\n            },\n            {\n              \"title\": \"component-koa-middleware\",\n              \"slug\": \"component-koa-middleware\",\n              \"file\": \"reference/component-koa-middleware-402oMlpSmEIARQJLzn4qQg.immutable.md\",\n              \"category\": \"Communication\"\n            },\n            {\n              \"title\": \"Storable()\",\n              \"slug\": \"storable\",\n              \"file\": \"reference/storable-2maAYPX5kWufBfVolYVgdc.immutable.md\",\n              \"category\": \"Storage\"\n            },\n            {\n              \"title\": \"StorableProperty\",\n              \"slug\": \"storable-property\",\n              \"file\": \"reference/storable-property-5LuWR7uuQ1qdebK3iszZJO.immutable.md\",\n              \"category\": \"Storage\"\n            },\n            {\n              \"title\": \"StorableAttribute\",\n              \"slug\": \"storable-attribute\",\n              \"file\": \"reference/storable-attribute-7MiyqvA7PUf2E2YeoqA912.immutable.md\",\n              \"category\": \"Storage\"\n            },\n            {\n              \"title\": \"StorablePrimaryIdentifierAttribute\",\n              \"slug\": \"storable-primary-identifier-attribute\",\n              \"file\": \"reference/storable-primary-identifier-attribute-74IwGTycdA7NVAybBly3t1.immutable.md\",\n              \"category\": \"Storage\"\n            },\n            {\n              \"title\": \"StorableSecondaryIdentifierAttribute\",\n              \"slug\": \"storable-secondary-identifier-attribute\",\n              \"file\": \"reference/storable-secondary-identifier-attribute-6CqhR0kLxoLhtWU85EcsZU.immutable.md\",\n              \"category\": \"Storage\"\n            },\n            {\n              \"title\": \"StorableMethod\",\n              \"slug\": \"storable-method\",\n              \"file\": \"reference/storable-method-1rnW3aKFlFYZ3kIJdCGXRv.immutable.md\",\n              \"category\": \"Storage\"\n            },\n            {\n              \"title\": \"Query\",\n              \"slug\": \"query\",\n              \"file\": \"reference/query-Q04JuSZAcx8HGvYktmTlI.immutable.md\",\n              \"category\": \"Storage\"\n            },\n            {\n              \"title\": \"Index\",\n              \"slug\": \"index\",\n              \"file\": \"reference/index-1KeXILVBQNLQpRzBhIWjqB.immutable.md\",\n              \"category\": \"Storage\"\n            },\n            {\n              \"title\": \"Store\",\n              \"slug\": \"store\",\n              \"file\": \"reference/store-6BvlCSWkAb7smHsvoIGjv1.immutable.md\",\n              \"category\": \"Storage\"\n            },\n            {\n              \"title\": \"MongoDBStore\",\n              \"slug\": \"mongodb-store\",\n              \"file\": \"reference/mongodb-store-1yTYtEyVy4ZLkF4A2QFaAh.immutable.md\",\n              \"category\": \"Storage\"\n            },\n            {\n              \"title\": \"MemoryStore\",\n              \"slug\": \"memory-store\",\n              \"file\": \"reference/memory-store-78uJRKSOEyr1WnXttCnd9u.immutable.md\",\n              \"category\": \"Storage\"\n            },\n            {\n              \"title\": \"Routable()\",\n              \"slug\": \"routable\",\n              \"file\": \"reference/routable-3zt4vMLvzQR3aqOwoWd3xV.immutable.md\",\n              \"category\": \"Routing\"\n            },\n            {\n              \"title\": \"Route\",\n              \"slug\": \"route\",\n              \"file\": \"reference/route-1rS9AS4eda7qe7lWTU4WKF.immutable.md\",\n              \"category\": \"Routing\"\n            },\n            {\n              \"title\": \"Router\",\n              \"slug\": \"router\",\n              \"file\": \"reference/router-5zhzJL9MYM1yaHY6CXlkso.immutable.md\",\n              \"category\": \"Routing\"\n            },\n            {\n              \"title\": \"BrowserRouter\",\n              \"slug\": \"browser-router\",\n              \"file\": \"reference/browser-router-6EIyPCgoPYsWmDmpyKMhce.immutable.md\",\n              \"category\": \"Routing\"\n            },\n            {\n              \"title\": \"MemoryRouter\",\n              \"slug\": \"memory-router\",\n              \"file\": \"reference/memory-router-TGoBOhUXFPp3iDyKA9Sn0.immutable.md\",\n              \"category\": \"Routing\"\n            },\n            {\n              \"title\": \"WithRoles()\",\n              \"slug\": \"with-roles\",\n              \"file\": \"reference/with-roles-2PYi0037F3Mxg80b5YKb26.immutable.md\",\n              \"category\": \"Authorization\"\n            },\n            {\n              \"title\": \"Role\",\n              \"slug\": \"role\",\n              \"file\": \"reference/role-UodTLJi6Lg6UPXOKZdp8K.immutable.md\",\n              \"category\": \"Authorization\"\n            },\n            {\n              \"title\": \"aws-integration\",\n              \"slug\": \"aws-integration\",\n              \"file\": \"reference/aws-integration-3iPKbtjOS0uDgsZKk6Q0Kp.immutable.md\",\n              \"category\": \"Integrations\"\n            },\n            {\n              \"title\": \"react-integration\",\n              \"slug\": \"react-integration\",\n              \"file\": \"reference/react-integration-3EgAB99IhnithzdqiytvwF.immutable.md\",\n              \"category\": \"Integrations\"\n            },\n            {\n              \"title\": \"Observable()\",\n              \"slug\": \"observable\",\n              \"file\": \"reference/observable-6JJJVlFHPe2kPQ4NhsJg7O.immutable.md\",\n              \"category\": \"Utilities\"\n            }\n          ]\n        }\n      ]\n    },\n    \"v2\": {\n      \"books\": [\n        {\n          \"title\": \"Getting Started\",\n          \"slug\": \"introduction\",\n          \"chapters\": [\n            {\n              \"title\": \"Introduction\",\n              \"slug\": \"introduction\",\n              \"file\": \"introduction/introduction-1iLUBREWe2BB65qw9G4uzm.immutable.md\"\n            },\n            {\n              \"title\": \"Hello, World!\",\n              \"slug\": \"hello-world\",\n              \"file\": \"introduction/hello-world/hello-world-2FHBCgFRhZX9qH9O1RkTlZ.immutable.md\"\n            },\n            {\n              \"title\": \"Storing Data\",\n              \"slug\": \"storing-data\",\n              \"file\": \"introduction/storing-data-40jzWiVleR8ZE0fqfGTbO2.immutable.md\"\n            },\n            {\n              \"title\": \"Handling Authorization\",\n              \"slug\": \"handling-authorization\",\n              \"file\": \"introduction/handling-authorization-1sLAebuD81FLlSvJbAuJmZ.immutable.md\"\n            }\n          ]\n        },\n        {\n          \"title\": \"Concepts\",\n          \"slug\": \"concepts\",\n          \"chapters\": [\n            {\n              \"title\": \"Coming Soon\",\n              \"slug\": \"coming-soon\",\n              \"file\": \"concepts/coming-soon-38q4cuyCWekHuz36oHyMKG.immutable.md\"\n            }\n          ]\n        },\n        {\n          \"title\": \"Guides\",\n          \"slug\": \"guides\",\n          \"chapters\": [\n            {\n              \"title\": \"Coming Soon\",\n              \"slug\": \"coming-soon\",\n              \"file\": \"guides/coming-soon-6TWZbDxh3EVysir57k1tga.immutable.md\"\n            }\n          ]\n        },\n        {\n          \"title\": \"Reference\",\n          \"slug\": \"reference\",\n          \"chapters\": [\n            {\n              \"title\": \"Component\",\n              \"slug\": \"component\",\n              \"file\": \"reference/component-1zBrZVglO2cjDP9aO87pOR.immutable.md\",\n              \"category\": \"Basics\"\n            },\n            {\n              \"title\": \"EmbeddedComponent\",\n              \"slug\": \"embedded-component\",\n              \"file\": \"reference/embedded-component-3YdGqfLNxl8c001AvWxHZ6.immutable.md\",\n              \"category\": \"Basics\"\n            },\n            {\n              \"title\": \"Property\",\n              \"slug\": \"property\",\n              \"file\": \"reference/property-bs5Xm31fCQXQkoZL24prZ.immutable.md\",\n              \"category\": \"Basics\"\n            },\n            {\n              \"title\": \"Attribute\",\n              \"slug\": \"attribute\",\n              \"file\": \"reference/attribute-4IRansPGIm4E3gunH3Ztlq.immutable.md\",\n              \"category\": \"Basics\"\n            },\n            {\n              \"title\": \"IdentifierAttribute\",\n              \"slug\": \"identifier-attribute\",\n              \"file\": \"reference/identifier-attribute-2w8hckqCKGHPurgIGY5vhT.immutable.md\",\n              \"category\": \"Basics\"\n            },\n            {\n              \"title\": \"PrimaryIdentifierAttribute\",\n              \"slug\": \"primary-identifier-attribute\",\n              \"file\": \"reference/primary-identifier-attribute-5EqFynrkNUTa9DxYYmbmOl.immutable.md\",\n              \"category\": \"Basics\"\n            },\n            {\n              \"title\": \"SecondaryIdentifierAttribute\",\n              \"slug\": \"secondary-identifier-attribute\",\n              \"file\": \"reference/secondary-identifier-attribute-27lBtqHBopmgIAWwmeoLBg.immutable.md\",\n              \"category\": \"Basics\"\n            },\n            {\n              \"title\": \"Method\",\n              \"slug\": \"method\",\n              \"file\": \"reference/method-MAFTDipVXsOXj4o2CJLNQ.immutable.md\",\n              \"category\": \"Basics\"\n            },\n            {\n              \"title\": \"ValueType\",\n              \"slug\": \"value-type\",\n              \"file\": \"reference/value-type-FqKdGxNpEPIDVR56kq4NN.immutable.md\",\n              \"category\": \"Basics\"\n            },\n            {\n              \"title\": \"Sanitizer\",\n              \"slug\": \"sanitizer\",\n              \"file\": \"reference/sanitizer-3hc34yGhzbK1cupoMtP6Dv.immutable.md\",\n              \"category\": \"Basics\"\n            },\n            {\n              \"title\": \"Validator\",\n              \"slug\": \"validator\",\n              \"file\": \"reference/validator-2aTKblcN30ADdXqrI3bHXv.immutable.md\",\n              \"category\": \"Basics\"\n            },\n            {\n              \"title\": \"AttributeSelector\",\n              \"slug\": \"attribute-selector\",\n              \"file\": \"reference/attribute-selector-76ksImknctJspHLg12sRpR.immutable.md\",\n              \"category\": \"Basics\"\n            },\n            {\n              \"title\": \"IdentityMap\",\n              \"slug\": \"identity-map\",\n              \"file\": \"reference/identity-map-7sLdA5rsLvflf8T8OsZfGO.immutable.md\",\n              \"category\": \"Basics\"\n            },\n            {\n              \"title\": \"ComponentClient\",\n              \"slug\": \"component-client\",\n              \"file\": \"reference/component-client-69HLk6gKzg5DfWcVC2bQWS.immutable.md\",\n              \"category\": \"Communication\"\n            },\n            {\n              \"title\": \"ComponentServer\",\n              \"slug\": \"component-server\",\n              \"file\": \"reference/component-server-i0AsOVFbGCJUrJ9mJyjPh.immutable.md\",\n              \"category\": \"Communication\"\n            },\n            {\n              \"title\": \"ComponentHTTPClient\",\n              \"slug\": \"component-http-client\",\n              \"file\": \"reference/component-http-client-2rHYFMcsawI16EvGk8372w.immutable.md\",\n              \"category\": \"Communication\"\n            },\n            {\n              \"title\": \"ComponentHTTPServer\",\n              \"slug\": \"component-http-server\",\n              \"file\": \"reference/component-http-server-5aWTLSzVvm0WohfP6af7OP.immutable.md\",\n              \"category\": \"Communication\"\n            },\n            {\n              \"title\": \"component-express-middleware\",\n              \"slug\": \"component-express-middleware\",\n              \"file\": \"reference/component-express-middleware-7KGBerWY9jB9r9eOjwLmPq.immutable.md\",\n              \"category\": \"Communication\"\n            },\n            {\n              \"title\": \"component-koa-middleware\",\n              \"slug\": \"component-koa-middleware\",\n              \"file\": \"reference/component-koa-middleware-6aVaQsSgkzWt35XyqSMuqx.immutable.md\",\n              \"category\": \"Communication\"\n            },\n            {\n              \"title\": \"Storable()\",\n              \"slug\": \"storable\",\n              \"file\": \"reference/storable-3UMrfeCfFVaVttjxtmCvVy.immutable.md\",\n              \"category\": \"Storage\"\n            },\n            {\n              \"title\": \"StorableProperty\",\n              \"slug\": \"storable-property\",\n              \"file\": \"reference/storable-property-6Ooq1wLy5pGx7Jb1qRrRyf.immutable.md\",\n              \"category\": \"Storage\"\n            },\n            {\n              \"title\": \"StorableAttribute\",\n              \"slug\": \"storable-attribute\",\n              \"file\": \"reference/storable-attribute-1EYIvaUK2WvHmV3OxG5IzJ.immutable.md\",\n              \"category\": \"Storage\"\n            },\n            {\n              \"title\": \"StorablePrimaryIdentifierAttribute\",\n              \"slug\": \"storable-primary-identifier-attribute\",\n              \"file\": \"reference/storable-primary-identifier-attribute-7qq06XjPnNRXumxcngJvZv.immutable.md\",\n              \"category\": \"Storage\"\n            },\n            {\n              \"title\": \"StorableSecondaryIdentifierAttribute\",\n              \"slug\": \"storable-secondary-identifier-attribute\",\n              \"file\": \"reference/storable-secondary-identifier-attribute-1TKIxZX9JZ2aGFU5iEmUSv.immutable.md\",\n              \"category\": \"Storage\"\n            },\n            {\n              \"title\": \"StorableMethod\",\n              \"slug\": \"storable-method\",\n              \"file\": \"reference/storable-method-1o1egWUfiZzxO6y7Sda4rb.immutable.md\",\n              \"category\": \"Storage\"\n            },\n            {\n              \"title\": \"Query\",\n              \"slug\": \"query\",\n              \"file\": \"reference/query-146gEiaHLp06V3LqhxRCzC.immutable.md\",\n              \"category\": \"Storage\"\n            },\n            {\n              \"title\": \"Index\",\n              \"slug\": \"index\",\n              \"file\": \"reference/index-1Uz5JX1XB7V67nfM6tvnSV.immutable.md\",\n              \"category\": \"Storage\"\n            },\n            {\n              \"title\": \"Store\",\n              \"slug\": \"store\",\n              \"file\": \"reference/store-1mGBVGCiO4K5X9dBakik9Z.immutable.md\",\n              \"category\": \"Storage\"\n            },\n            {\n              \"title\": \"MongoDBStore\",\n              \"slug\": \"mongodb-store\",\n              \"file\": \"reference/mongodb-store-37qsYI8A5ctkFlHTDVIc3O.immutable.md\",\n              \"category\": \"Storage\"\n            },\n            {\n              \"title\": \"MemoryStore\",\n              \"slug\": \"memory-store\",\n              \"file\": \"reference/memory-store-3QHbAhGolblHx4OY17rEP.immutable.md\",\n              \"category\": \"Storage\"\n            },\n            {\n              \"title\": \"Routable()\",\n              \"slug\": \"routable\",\n              \"file\": \"reference/routable-7zjaksbkNa8sbNfqNnEKUu.immutable.md\",\n              \"category\": \"Routing\"\n            },\n            {\n              \"title\": \"Addressable\",\n              \"slug\": \"addressable\",\n              \"file\": \"reference/addressable-6n5npS1cCoPKLkFrYU0Pk7.immutable.md\",\n              \"category\": \"Routing\"\n            },\n            {\n              \"title\": \"Route\",\n              \"slug\": \"route\",\n              \"file\": \"reference/route-3O1kMMGl4yZc9LGxH38Z3G.immutable.md\",\n              \"category\": \"Routing\"\n            },\n            {\n              \"title\": \"Wrapper\",\n              \"slug\": \"wrapper\",\n              \"file\": \"reference/wrapper-4uhJFGS18pbZTt9qdtLnL2.immutable.md\",\n              \"category\": \"Routing\"\n            },\n            {\n              \"title\": \"Navigator\",\n              \"slug\": \"navigator\",\n              \"file\": \"reference/navigator-5vNC9hp62PFUvxh0PLQcJ9.immutable.md\",\n              \"category\": \"Routing\"\n            },\n            {\n              \"title\": \"BrowserNavigator\",\n              \"slug\": \"browser-navigator\",\n              \"file\": \"reference/browser-navigator-eYIxYKNFdRYiKTxIWAjqd.immutable.md\",\n              \"category\": \"Routing\"\n            },\n            {\n              \"title\": \"MemoryNavigator\",\n              \"slug\": \"memory-navigator\",\n              \"file\": \"reference/memory-navigator-5wC9TEZn2cen7LuV4cGPOj.immutable.md\",\n              \"category\": \"Routing\"\n            },\n            {\n              \"title\": \"WithRoles()\",\n              \"slug\": \"with-roles\",\n              \"file\": \"reference/with-roles-CcLPnrWKVYxnZXgNTqVYi.immutable.md\",\n              \"category\": \"Authorization\"\n            },\n            {\n              \"title\": \"Role\",\n              \"slug\": \"role\",\n              \"file\": \"reference/role-3zJ3w6whE8RucN9kAhS5Hv.immutable.md\",\n              \"category\": \"Authorization\"\n            },\n            {\n              \"title\": \"aws-integration\",\n              \"slug\": \"aws-integration\",\n              \"file\": \"reference/aws-integration-3Rt2txpqioYQsmijgSqqwF.immutable.md\",\n              \"category\": \"Integrations\"\n            },\n            {\n              \"title\": \"react-integration\",\n              \"slug\": \"react-integration\",\n              \"file\": \"reference/react-integration-4BgvR6gvTuqLdpNymyUYS1.immutable.md\",\n              \"category\": \"Integrations\"\n            },\n            {\n              \"title\": \"Observable()\",\n              \"slug\": \"observable\",\n              \"file\": \"reference/observable-7alzx5bqV1gD2Aj60hWQ0L.immutable.md\",\n              \"category\": \"Utilities\"\n            }\n          ]\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "website/frontend/src/index.ts",
    "content": "import {ComponentHTTPClient} from '@layr/component-http-client';\nimport {Storable} from '@layr/storable';\n\nimport type {Application as BackendApplication} from '../../backend/src/components/application';\nimport {extendApplication} from './components/application';\n\nexport default async () => {\n  const client = new ComponentHTTPClient(process.env.BACKEND_URL!, {\n    mixins: [Storable],\n    async retryFailedRequests() {\n      return confirm('Sorry, a network error occurred. Would you like to retry?');\n    }\n  });\n\n  const BackendApplicationProxy = (await client.getComponent()) as typeof BackendApplication;\n\n  const Application = extendApplication(BackendApplicationProxy);\n\n  return Application;\n};\n"
  },
  {
    "path": "website/frontend/src/markdown.tsx",
    "content": "import {isInternalURL} from '@layr/browser-navigator';\nimport {jsx} from '@emotion/react';\nimport {marked} from 'marked';\n// @ts-ignore\nimport highlightJS from 'highlight.js/lib/core';\n// @ts-ignore\nimport typescript from 'highlight.js/lib/languages/typescript';\n// @ts-ignore\nimport json from 'highlight.js/lib/languages/json';\n// @ts-ignore\nimport xml from 'highlight.js/lib/languages/xml';\n// @ts-ignore\nimport bash from 'highlight.js/lib/languages/bash';\n// @ts-ignore\nimport plainText from 'highlight.js/lib/languages/plaintext';\nimport DOMPurify from 'dompurify';\n\nhighlightJS.registerLanguage('typescript', typescript);\nhighlightJS.registerLanguage('json', json);\nhighlightJS.registerLanguage('html', xml);\nhighlightJS.registerLanguage('bash', bash);\nhighlightJS.registerLanguage('plainText', plainText);\n\nexport function Markdown({languageFilter, children}: {languageFilter?: string; children: string}) {\n  let html = marked(children, {\n    highlight: (code, language) => {\n      if (languageFilter !== undefined) {\n        const matches = code.match(/^\\/\\/ (\\w+)\\n\\n/);\n\n        if (matches !== null) {\n          const languageSpecifier = matches[1].toLocaleLowerCase();\n\n          if (languageSpecifier !== languageFilter) {\n            return ''; // The code must be filtered out\n          }\n\n          code = code.slice(matches[0].length); // Remove the language specifier from the code\n        }\n      }\n\n      if (language === '') {\n        language = 'ts'; // Use TS as default\n      }\n\n      if (language === 'js') {\n        language = 'ts'; // Always highlight JS as TS\n      }\n\n      return highlightJS.highlight(code, {language}).value;\n    }\n  });\n\n  if (languageFilter !== undefined) {\n    // Finish removing the filtered out languages\n    html = html.replace(/<pre><code( class=\"language-\\w+\")?>\\s?<\\/code><\\/pre>\\n/g, '');\n\n    // Handle the '<if language>' tags\n    html = html.replace(\n      /<!-- <if language=\"(\\w+)\"> -->([^]*?)<!-- <\\/if> -->/g,\n      (_, language, content) => {\n        return language === languageFilter ? content : '';\n      }\n    );\n  }\n\n  html = DOMPurify.sanitize(html, {ADD_TAGS: ['badge']});\n\n  // Handle badge tag\n  html = html.replace(/<badge(?: type=\"([\\w-]+)\")?>([\\w ]+)<\\/badge>/g, (_, type, name) => {\n    return `<span class='badge${type !== undefined ? ` badge-${type}` : ''}'>${name}</span>`;\n  });\n\n  // Handle custom header id\n  // Replace: <h4 id=\"creation-creation\">Creation {#creation}</h4>\n  // With: <h4 id=\"creation\">Creation</h4>\n  html = html.replace(/<h\\d id=\"([\\w-]+)\">.+\\{\\#([\\w-]+)\\}<\\/h\\d>/g, (match, currentId, newId) => {\n    match = match.replace(` id=\"${currentId}\">`, ` id=\"${newId}\">`);\n    match = match.replace(` {#${newId}}<`, `<`);\n    return match;\n  });\n\n  html = html.replace(/<h\\d id=\"([\\w-]+)\">/g, (match, id) => {\n    match += `<a href=\"#${id}\" class=\"anchor\" aria-hidden=\"true\">&nbsp;</a>`;\n    return match;\n  });\n\n  if (process.env.NODE_ENV === 'development') {\n    const localURL = new URL(window.location.href).origin;\n    html = html.replace(/https:\\/\\/layrjs.com/g, localURL);\n  }\n\n  // Handle link clicks\n  // Replace: <a href=\"target\">text</a>\n  // With: <a href=\"target\" onclick=\"...\">text</a>\n  html = html.replace(/<a href=\"([^\"]+)\">.*?<\\/a>/g, (match, url) => {\n    if (isInternalURL(url)) {\n      const onClick = `document.body.dispatchEvent(new CustomEvent('layrNavigatorNavigate', {detail: {url: '${url}'}})); return false;`;\n      match = match.replace(`<a href=\"${url}\">`, `<a href=\"${url}\" onclick=\"${onClick}\">`);\n    }\n\n    return match;\n  });\n\n  return <div dangerouslySetInnerHTML={{__html: html}} />;\n}\n\nexport function InlineMarkdown({children: markdown}: {children: string}) {\n  markdown = markdown.replace(/\\n/g, '  \\n');\n\n  let html = marked.parseInline(markdown);\n\n  html = DOMPurify.sanitize(html);\n\n  return <span dangerouslySetInnerHTML={{__html: html}} />;\n}\n"
  },
  {
    "path": "website/frontend/src/styles.ts",
    "content": "import {useTheme, Theme} from '@emotion/react';\nimport memoize from 'lodash/memoize';\n\nexport function getGlobalStyles(theme: Theme) {\n  return {\n    // === Header anchors ===\n\n    'h1 .anchor': {\n      display: 'none'\n    },\n    'h1:hover .anchor': {\n      display: 'inline'\n    },\n    'h2 .anchor': {\n      display: 'none'\n    },\n    'h2:hover .anchor': {\n      display: 'inline'\n    },\n    'h3 .anchor': {\n      display: 'none'\n    },\n    'h3:hover .anchor': {\n      display: 'inline'\n    },\n    'h4 .anchor': {\n      display: 'none'\n    },\n    'h4:hover .anchor': {\n      display: 'inline'\n    },\n    'h5 .anchor': {\n      display: 'none'\n    },\n    'h5:hover .anchor': {\n      display: 'inline'\n    },\n    'h6 .anchor': {\n      display: 'none'\n    },\n    'h6:hover .anchor': {\n      display: 'inline'\n    },\n    '.anchor': {\n      float: 'left',\n      width: 20,\n      marginLeft: -20,\n      backgroundImage: `url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'%3E%3Cpath fill='%23888' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'%3E%3C/path%3E%3C/svg%3E\")`,\n      backgroundRepeat: 'no-repeat',\n      backgroundPosition: 'right 4px center'\n    },\n    '.anchor:hover': {\n      textDecoration: 'none'\n    },\n\n    // === Code blocks ===\n\n    'code, pre': {\n      fontFamily: \"Menlo, Consolas, 'Liberation Mono', monospace\"\n    },\n    'pre': {\n      display: 'table',\n      tableLayout: 'fixed',\n      width: '100%',\n      marginTop: '1.1rem',\n      marginBottom: '1.1rem',\n      padding: '.4rem .65rem .5rem .65rem',\n      fontSize: '.85rem',\n      color: theme.colors.text,\n      backgroundColor: theme.colors.background.highlighted,\n      borderRadius: 3\n    },\n    'code': {\n      padding: '.15rem .15rem',\n      fontSize: '90%',\n      wordBreak: 'break-word',\n      overflowWrap: 'anywhere',\n      color: theme.colors.text,\n      backgroundColor: theme.colors.background.highlighted,\n      borderRadius: 3\n    },\n    'h1 code, h2 code, h3 code, h4 code, h5 code, h6 code': {\n      fontSize: '100%'\n    },\n    'a code': {\n      color: theme.colors.primary.normal\n    },\n    'pre code': {\n      display: 'table-cell !important',\n      overflowX: 'auto',\n      padding: 0,\n      fontSize: 'inherit',\n      wordBreak: 'normal',\n      overflowWrap: 'normal',\n      color: 'inherit',\n      backgroundColor: 'transparent',\n      borderRadius: 0\n    },\n    'pre + h5': {\n      marginTop: '1.6rem'\n    },\n    '.hljs-comment, .hljs-quote': {\n      color: theme.colors.text.muted,\n      fontStyle: 'italic'\n    },\n    '.hljs-doctag, .hljs-formula': {\n      color: theme.colors.tertiary.normal\n    },\n    '.hljs-keyword': {\n      color: theme.colors.secondary.normal\n    },\n    '.hljs-section, .hljs-name, .hljs-selector-tag, .hljs-deletion, .hljs-subst': {\n      color: theme.colors.text.normal\n    },\n    '.hljs-tag': {\n      color: theme.colors.primary.normal\n    },\n    '.hljs-tag .hljs-name': {\n      color: theme.colors.primary.normal\n    },\n    '.hljs-literal': {\n      color: theme.colors.tertiary.normal\n    },\n    '.hljs-string, .hljs-regexp, .hljs-addition, .hljs-attribute, .hljs-meta-string': {\n      color: theme.colors.tertiary.normal\n    },\n    '.hljs-built_in': {\n      color: theme.colors.secondary.normal\n    },\n    '.hljs-class .hljs-title': {\n      color: theme.colors.text.normal\n    },\n    '.hljs-attr': {\n      color: theme.colors.secondary.normal\n    },\n    '.hljs-variable, .hljs-template-variable, .hljs-type, .hljs-selector-class, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-number': {\n      color: theme.colors.tertiary.normal\n    },\n    '.hljs-symbol, .hljs-bullet, .hljs-link, .hljs-meta, .hljs-selector-id': {\n      color: theme.colors.primary.normal\n    },\n    '.hljs-emphasis': {\n      fontStyle: 'italic'\n    },\n    '.hljs-strong': {\n      fontWeight: 'bold'\n    },\n    '.hljs-link': {\n      textDecoration: 'underline'\n    },\n\n    // === Badges ===\n\n    '.badge': {\n      display: 'inline-block',\n      color: theme.colors.text,\n      backgroundColor: theme.colors.background.highlighted,\n      padding: '.25rem .4rem .3rem .4rem',\n      fontSize: '.7rem',\n      fontWeight: 600,\n      letterSpacing: 0.3,\n      textTransform: 'uppercase',\n      lineHeight: 1,\n      textAlign: 'center',\n      whiteSpace: 'nowrap',\n      verticalAlign: 'middle',\n      borderRadius: '.25rem'\n    },\n    '.badge-primary': {\n      color: theme.colors.primary.textOnNormal,\n      backgroundColor: theme.colors.primary.normal\n    },\n    '.badge-secondary': {\n      color: theme.colors.secondary.textOnNormal,\n      backgroundColor: theme.colors.secondary.normal\n    },\n    '.badge-tertiary': {\n      color: theme.colors.tertiary.textOnNormal,\n      backgroundColor: theme.colors.tertiary.normal\n    },\n    '.badge-outline': {\n      color: theme.colors.text.muted,\n      backgroundColor: 'inherit',\n      borderWidth: 1,\n      borderStyle: 'solid',\n      borderColor: theme.colors.text.muted\n    },\n    '.badge-primary-outline': {\n      color: theme.colors.primary.highlighted,\n      backgroundColor: 'inherit',\n      borderWidth: 1,\n      borderStyle: 'solid',\n      borderColor: theme.colors.primary.highlighted\n    },\n    '.badge-secondary-outline': {\n      color: theme.colors.secondary.highlighted,\n      backgroundColor: 'inherit',\n      borderWidth: 1,\n      borderStyle: 'solid',\n      borderColor: theme.colors.secondary.highlighted\n    },\n    '.badge-tertiary-outline': {\n      color: theme.colors.tertiary.normal,\n      backgroundColor: 'inherit',\n      borderWidth: 1,\n      borderStyle: 'solid',\n      borderColor: theme.colors.tertiary.normal\n    }\n  };\n}\n\nconst getStyles = memoize(function getStyles(theme: Theme) {\n  return {\n    unstyledList: {paddingLeft: 0, listStyle: 'none'},\n\n    hiddenLink: {\n      'color': 'inherit',\n      ':hover': {color: 'inherit'}\n    },\n\n    menuItemLink: {\n      'color': theme.colors.primary.normal,\n      'cursor': 'pointer',\n      ':hover': {\n        color: theme.colors.primary.highlighted,\n        textDecoration: 'none'\n      }\n    }\n  };\n});\n\nexport function useStyles() {\n  return getStyles(useTheme());\n}\n"
  },
  {
    "path": "website/frontend/src/ui.tsx",
    "content": "import {useMemo} from 'react';\nimport {useDelay} from '@layr/react-integration';\nimport {jsx, useTheme} from '@emotion/react';\nimport {useWindowHeight} from '@react-hook/window-size';\nimport {Helmet} from 'react-helmet';\nimport {formatError} from '@layr/utilities';\n\nimport {InlineMarkdown} from './markdown';\nimport somethingWrong from './assets/something-wrong.png';\n\nexport function FullHeight({\n  id,\n  className,\n  children\n}: {\n  id?: string;\n  className?: string;\n  children: React.ReactNode;\n}) {\n  const height = useWindowHeight({initialHeight: 600});\n\n  return (\n    <div id={id} className={className} css={{minHeight: height}}>\n      {children}\n    </div>\n  );\n}\n\nexport function FeatureSection({\n  title,\n  description,\n  children\n}: {\n  title: string;\n  description: string;\n  children: React.ReactNode;\n}) {\n  const theme = useTheme();\n\n  return (\n    <div\n      css={theme.responsive({\n        display: 'flex',\n        justifyContent: 'center',\n        alignItems: 'center',\n        padding: ['3rem 1.5rem', , '3rem 15px']\n      })}\n    >\n      <div css={{display: 'flex', flexDirection: 'column', alignItems: 'center'}}>\n        <h3\n          css={theme.responsive({\n            fontSize: [, , '1.563rem'],\n            textAlign: 'center'\n          })}\n        >\n          {title}\n        </h3>\n        <div\n          css={theme.responsive({\n            fontSize: ['1.25rem', , '1rem'],\n            color: theme.colors.text.muted,\n            textAlign: 'center'\n          })}\n        >\n          <InlineMarkdown>{description}</InlineMarkdown>\n        </div>\n        {children}\n      </div>\n    </div>\n  );\n}\n\nexport function Title({children: title}: {children?: string}) {\n  if (title === undefined) {\n    title = 'Layr';\n  } else {\n    title = 'Layr – ' + title;\n  }\n\n  return (\n    <Helmet>\n      <title>{title}</title>\n    </Helmet>\n  );\n}\n\nexport function ErrorMessage({children}: {children: string | Error}) {\n  const message = typeof children === 'string' ? children : formatError(children);\n\n  return (\n    <div\n      css={{\n        width: '100%',\n        padding: '3rem 15px',\n        display: 'flex',\n        flexDirection: 'column',\n        alignItems: 'center'\n      }}\n    >\n      <img src={somethingWrong} alt=\"Something went wrong\" css={{width: 350, maxWidth: '100%'}} />\n      <h4 css={{marginTop: '3rem', textAlign: 'center'}}>{message}</h4>\n    </div>\n  );\n}\n\nexport function LoadingSpinner({delay}: {delay?: number}) {\n  const style = useMemo(\n    () => ({\n      borderRadius: '50%',\n      width: '40px',\n      height: '40px',\n      margin: '90px auto',\n      position: 'relative' as const,\n      borderTop: '3px solid rgba(0, 0, 0, 0.1)',\n      borderRight: '3px solid rgba(0, 0, 0, 0.1)',\n      borderBottom: '3px solid rgba(0, 0, 0, 0.1)',\n      borderLeft: '3px solid #818a91',\n      transform: 'translateZ(0)',\n      animation: 'loading-spinner 0.5s infinite linear'\n    }),\n    []\n  );\n\n  return (\n    <Delayed duration={delay}>\n      <div className=\"loading-spinner\" style={style}>\n        <style>\n          {`\n        @keyframes loading-spinner {\n          0% {transform: rotate(0deg);}\n          100% {transform: rotate(360deg);}\n        }\n        `}\n        </style>\n      </div>\n    </Delayed>\n  );\n}\n\nexport function Delayed({\n  duration = 500,\n  children\n}: {\n  duration?: number;\n  children: React.ReactElement;\n}) {\n  const [isElapsed] = useDelay(duration);\n\n  if (isElapsed) {\n    return children;\n  }\n\n  return null;\n}\n"
  },
  {
    "path": "website/frontend/tsconfig.json",
    "content": "{\n  \"extends\": \"@boostr/tsconfig\",\n  \"include\": [\"src/**/*\"],\n  \"compilerOptions\": {\n    \"jsxFactory\": \"jsx\"\n  }\n}\n"
  },
  {
    "path": "website/package.json",
    "content": "{\n  \"name\": \"layr-website\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"devDependencies\": {\n    \"boostr\": \"^2.0.167\"\n  }\n}\n"
  },
  {
    "path": "website/redirection/package.json",
    "content": "{\n  \"name\": \"layr-website-redirection\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"author\": \"Manuel Vila <hi@mvila.me>\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"deploy\": \"FRONTEND_URL=https://liaison.dev simple-deployment\"\n  },\n  \"dependencies\": {},\n  \"devDependencies\": {\n    \"simple-deployment\": \"^0.1.41\"\n  }\n}\n"
  },
  {
    "path": "website/redirection/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta http-equiv=\"x-ua-compatible\" content=\"ie=edge\" />\n    <style>\n      body {\n        color: #eceff1;\n        background-color: #263238;\n        font-family: Arial, Helvetica, sans-serif;\n        font-size: 18px;\n        line-height: 1.3;\n      }\n\n      h1 {\n        margin: 0;\n        font-size: 30px;\n      }\n\n      a {\n        color: #00a9f4;\n        text-decoration: none;\n      }\n\n      a:hover {\n        text-decoration: underline;\n      }\n\n      .panel {\n        margin: 50px auto 0 auto;\n        max-width: 450px;\n        padding: 25px;\n        text-align: center;\n        border: 1px solid #888;\n        border-radius: 3px;\n      }\n\n      h1 {\n        margin-top: -7px;\n      }\n\n      p {\n        margin: 20px 0 27px 0;\n        opacity: .8;\n      }\n\n      .button {\n        padding: 15px;\n        font-size: 25px;\n        text-align: center;\n        color: white;\n        background-color: #ff4081;\n        border-radius: 3px;\n        cursor: pointer;\n      }\n\n      .button:hover {\n        background-color: #ff4081a0;\n      }\n    </style>\n  </head>\n  <body>\n    <noscript><p>Sorry, this site requires JavaScript to be enabled.</p></noscript>\n\n    <script>\n      function visitNewWebsite() {\n        window.location.host = \"layrjs.com\";\n      }\n\n      setTimeout(visitNewWebsite, 5 * 1000);\n    </script>\n\n    <div class=\"panel\">\n      <h1>Liaison Is Now Called Layr</h1>\n\n      <p>You will be redirected to the <a href=\"https://layrjs.com\">new website</a>.</p>\n\n      <div class=\"button\" onclick=\"visitNewWebsite();\">Visit the new website</div>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "website/redirection/simple-deployment.config.js",
    "content": "module.exports = () => {\n  const frontendURL = process.env.FRONTEND_URL;\n\n  if (!frontendURL) {\n    throw new Error(`'FRONTEND_URL' environment variable is missing`);\n  }\n\n  const domainName = new URL(frontendURL).hostname;\n\n  return {\n    type: 'website',\n    provider: 'aws',\n    domainName,\n    files: ['./public'],\n    customErrors: [{errorCode: 404, responseCode: 200, responsePage: 'index.html'}],\n    aws: {\n      region: 'us-west-2',\n      cloudFront: {\n        priceClass: 'PriceClass_100'\n      }\n    }\n  };\n};\n"
  }
]