Repository: liaisonjs/liaison Branch: master Commit: 10a7e9f521a6 Files: 576 Total size: 2.3 MB Directory structure: gitextract_vbywh81w/ ├── .editorconfig ├── .gitignore ├── .npm-dev-registry.json ├── .prettierignore ├── .vscode/ │ └── extensions.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── assets/ │ ├── frontend-backend.excalidraw │ ├── frontend-webapi-backend.excalidraw │ ├── typical-stack-vs-layr-stack-light-mode.excalidraw │ └── typical-stack-vs-layr-stack.excalidraw ├── docs/ │ ├── build.js │ ├── contents/ │ │ ├── concepts/ │ │ │ └── coming-soon.md │ │ ├── guides/ │ │ │ └── coming-soon.md │ │ ├── index.json │ │ └── introduction/ │ │ ├── handling-authorization.md │ │ ├── hello-world/ │ │ │ └── hello-world.md │ │ ├── introduction.md │ │ └── storing-data.md │ └── package.json ├── examples/ │ ├── v1/ │ │ ├── counter/ │ │ │ ├── README.md │ │ │ ├── babel.config.json │ │ │ ├── jsconfig.json │ │ │ ├── package.json │ │ │ └── src/ │ │ │ ├── backend.js │ │ │ └── frontend.js │ │ ├── counter-with-create-react-app-ts/ │ │ │ ├── README.md │ │ │ ├── backend/ │ │ │ │ ├── package.json │ │ │ │ ├── src/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ └── counter.ts │ │ │ │ │ ├── http-server.ts │ │ │ │ │ └── server.ts │ │ │ │ └── tsconfig.json │ │ │ ├── frontend/ │ │ │ │ ├── .gitignore │ │ │ │ ├── package.json │ │ │ │ ├── public/ │ │ │ │ │ ├── index.html │ │ │ │ │ ├── manifest.json │ │ │ │ │ └── robots.txt │ │ │ │ ├── src/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ └── counter.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── react-app-env.d.ts │ │ │ │ └── tsconfig.json │ │ │ └── package.json │ │ ├── counter-with-esbuild-ts/ │ │ │ ├── README.md │ │ │ ├── backend/ │ │ │ │ ├── package.json │ │ │ │ ├── src/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ └── counter.ts │ │ │ │ │ ├── http-server.ts │ │ │ │ │ └── server.ts │ │ │ │ └── tsconfig.json │ │ │ ├── frontend/ │ │ │ │ ├── package.json │ │ │ │ ├── src/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ └── counter.tsx │ │ │ │ │ ├── index.html │ │ │ │ │ └── index.tsx │ │ │ │ └── tsconfig.json │ │ │ └── package.json │ │ ├── counter-with-http/ │ │ │ ├── README.md │ │ │ ├── babel.config.json │ │ │ ├── jsconfig.json │ │ │ ├── package.json │ │ │ └── src/ │ │ │ ├── backend.js │ │ │ └── frontend.js │ │ ├── counter-with-parcel-ts/ │ │ │ ├── README.md │ │ │ ├── backend/ │ │ │ │ ├── package.json │ │ │ │ ├── src/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ └── counter.ts │ │ │ │ │ ├── http-server.ts │ │ │ │ │ └── server.ts │ │ │ │ └── tsconfig.json │ │ │ ├── frontend/ │ │ │ │ ├── .gitignore │ │ │ │ ├── babel.config.js │ │ │ │ ├── package.json │ │ │ │ ├── src/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ └── counter.tsx │ │ │ │ │ ├── index.html │ │ │ │ │ └── index.tsx │ │ │ │ └── tsconfig.json │ │ │ └── package.json │ │ ├── counter-with-rollup-ts/ │ │ │ ├── README.md │ │ │ ├── backend/ │ │ │ │ ├── package.json │ │ │ │ ├── src/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ └── counter.ts │ │ │ │ │ ├── http-server.ts │ │ │ │ │ └── server.ts │ │ │ │ └── tsconfig.json │ │ │ ├── frontend/ │ │ │ │ ├── babel.config.js │ │ │ │ ├── package.json │ │ │ │ ├── rollup.config.js │ │ │ │ ├── src/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ └── counter.tsx │ │ │ │ │ ├── index.html │ │ │ │ │ └── index.tsx │ │ │ │ └── tsconfig.json │ │ │ └── package.json │ │ ├── guestbook-cli-js/ │ │ │ ├── README.md │ │ │ ├── babel.config.json │ │ │ ├── jsconfig.json │ │ │ ├── package.json │ │ │ └── src/ │ │ │ ├── backend.js │ │ │ └── frontend.js │ │ ├── guestbook-cli-ts/ │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── backend.ts │ │ │ │ └── frontend.ts │ │ │ └── tsconfig.json │ │ ├── guestbook-web-js/ │ │ │ ├── README.md │ │ │ ├── babel.config.json │ │ │ ├── jsconfig.json │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── backend.js │ │ │ │ ├── frontend.js │ │ │ │ └── index.html │ │ │ └── webpack.config.js │ │ ├── guestbook-web-ts/ │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── backend.ts │ │ │ │ ├── frontend.tsx │ │ │ │ └── index.html │ │ │ ├── tsconfig.json │ │ │ └── webpack.config.js │ │ ├── guestbook-web-with-authorization-js/ │ │ │ ├── README.md │ │ │ ├── babel.config.json │ │ │ ├── jsconfig.json │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── backend.js │ │ │ │ ├── frontend.js │ │ │ │ └── index.html │ │ │ └── webpack.config.js │ │ ├── guestbook-web-with-authorization-ts/ │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── backend.ts │ │ │ │ ├── frontend.tsx │ │ │ │ └── index.html │ │ │ ├── tsconfig.json │ │ │ └── webpack.config.js │ │ ├── guestbook-web-with-routes-js/ │ │ │ ├── README.md │ │ │ ├── babel.config.json │ │ │ ├── jsconfig.json │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── backend.js │ │ │ │ ├── frontend.js │ │ │ │ └── index.html │ │ │ └── webpack.config.js │ │ ├── guestbook-web-with-routes-ts/ │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── backend.ts │ │ │ │ ├── frontend.tsx │ │ │ │ └── index.html │ │ │ ├── tsconfig.json │ │ │ └── webpack.config.js │ │ ├── hello-world-js/ │ │ │ ├── README.md │ │ │ ├── babel.config.json │ │ │ ├── jsconfig.json │ │ │ ├── package.json │ │ │ └── src/ │ │ │ ├── backend.js │ │ │ └── frontend.js │ │ └── hello-world-ts/ │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── backend.ts │ │ │ └── frontend.ts │ │ └── tsconfig.json │ └── v2/ │ ├── hello-world-js/ │ │ ├── .editorconfig │ │ ├── .gitignore │ │ ├── .prettierignore │ │ ├── .vscode/ │ │ │ ├── extensions.json │ │ │ └── settings.json │ │ ├── README.md │ │ ├── backend/ │ │ │ ├── boostr.config.mjs │ │ │ ├── boostr.config.private-example.mjs │ │ │ ├── jsconfig.json │ │ │ ├── package.json │ │ │ └── src/ │ │ │ ├── components/ │ │ │ │ └── application.js │ │ │ └── index.js │ │ ├── boostr.config.mjs │ │ ├── database/ │ │ │ ├── .gitignore │ │ │ ├── boostr.config.mjs │ │ │ └── boostr.config.private-example.mjs │ │ ├── frontend/ │ │ │ ├── boostr.config.mjs │ │ │ ├── jsconfig.json │ │ │ ├── package.json │ │ │ └── src/ │ │ │ ├── components/ │ │ │ │ └── application.jsx │ │ │ └── index.js │ │ └── package.json │ └── hello-world-ts/ │ ├── .editorconfig │ ├── .gitignore │ ├── .prettierignore │ ├── .vscode/ │ │ ├── extensions.json │ │ └── settings.json │ ├── README.md │ ├── backend/ │ │ ├── boostr.config.mjs │ │ ├── boostr.config.private-example.mjs │ │ ├── package.json │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ └── application.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── boostr.config.mjs │ ├── database/ │ │ ├── .gitignore │ │ ├── boostr.config.mjs │ │ └── boostr.config.private-example.mjs │ ├── frontend/ │ │ ├── boostr.config.mjs │ │ ├── package.json │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ └── application.tsx │ │ │ └── index.ts │ │ └── tsconfig.json │ └── package.json ├── lerna.json ├── nx.json ├── package.json ├── packages/ │ ├── aws-integration/ │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ ├── lambda-execution-queue-sender.ts │ │ │ └── lambda-handler.ts │ │ └── tsconfig.json │ ├── browser-navigator/ │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── browser-navigator.ts │ │ │ ├── index.ts │ │ │ └── utilities.ts │ │ └── tsconfig.json │ ├── component/ │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── cloning.test.ts │ │ │ ├── cloning.ts │ │ │ ├── component.test.ts │ │ │ ├── component.ts │ │ │ ├── decorators.test.ts │ │ │ ├── decorators.ts │ │ │ ├── deserialization.test.ts │ │ │ ├── deserialization.ts │ │ │ ├── embedded-component.ts │ │ │ ├── forking.test.ts │ │ │ ├── forking.ts │ │ │ ├── identifiable-component.test.ts │ │ │ ├── identity-map.test.ts │ │ │ ├── identity-map.ts │ │ │ ├── index.ts │ │ │ ├── js-parser.ts │ │ │ ├── js-tests/ │ │ │ │ ├── decorators.test.js │ │ │ │ └── jsconfig.json │ │ │ ├── merging.test.ts │ │ │ ├── merging.ts │ │ │ ├── properties/ │ │ │ │ ├── attribute-selector.test.ts │ │ │ │ ├── attribute-selector.ts │ │ │ │ ├── attribute.test.ts │ │ │ │ ├── attribute.ts │ │ │ │ ├── identifier-attribute.test.ts │ │ │ │ ├── identifier-attribute.ts │ │ │ │ ├── index.ts │ │ │ │ ├── method.test.ts │ │ │ │ ├── method.ts │ │ │ │ ├── primary-identifier-attribute.test.ts │ │ │ │ ├── primary-identifier-attribute.ts │ │ │ │ ├── property.test.ts │ │ │ │ ├── property.ts │ │ │ │ ├── secondary-identifier-attribute.test.ts │ │ │ │ ├── secondary-identifier-attribute.ts │ │ │ │ └── value-types/ │ │ │ │ ├── any-value-type.ts │ │ │ │ ├── array-value-type.ts │ │ │ │ ├── boolean-value-type.ts │ │ │ │ ├── component-value-type.ts │ │ │ │ ├── date-value-type.ts │ │ │ │ ├── factory.test.ts │ │ │ │ ├── factory.ts │ │ │ │ ├── index.ts │ │ │ │ ├── number-value-type.ts │ │ │ │ ├── object-value-type.ts │ │ │ │ ├── regexp-value-type.ts │ │ │ │ ├── string-value-type.ts │ │ │ │ ├── value-type.test.ts │ │ │ │ └── value-type.ts │ │ │ ├── sanitization/ │ │ │ │ ├── index.ts │ │ │ │ ├── sanitizer-builders.test.ts │ │ │ │ ├── sanitizer-builders.ts │ │ │ │ ├── sanitizer.test.ts │ │ │ │ ├── sanitizer.ts │ │ │ │ ├── utilities.test.ts │ │ │ │ └── utilities.ts │ │ │ ├── serialization.test.ts │ │ │ ├── serialization.ts │ │ │ ├── utilities.test.ts │ │ │ ├── utilities.ts │ │ │ └── validation/ │ │ │ ├── index.ts │ │ │ ├── utilities.test.ts │ │ │ ├── utilities.ts │ │ │ ├── validator-builders.test.ts │ │ │ ├── validator-builders.ts │ │ │ ├── validator.test.ts │ │ │ └── validator.ts │ │ └── tsconfig.json │ ├── component-client/ │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── component-client.test.ts │ │ │ ├── component-client.ts │ │ │ ├── index.ts │ │ │ └── utilities.ts │ │ └── tsconfig.json │ ├── component-express-middleware/ │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── component-express-middleware.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── component-http-client/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── component-http-client.test.ts │ │ │ ├── component-http-client.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── component-http-server/ │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── component-http-server.test.ts │ │ │ ├── component-http-server.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── component-koa-middleware/ │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── component-koa-middleware.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── component-server/ │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── component-server.test.ts │ │ │ ├── component-server.ts │ │ │ ├── index.ts │ │ │ └── utilities.ts │ │ └── tsconfig.json │ ├── execution-queue/ │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── execution-queue.test.ts │ │ │ ├── execution-queue.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── integration-testing/ │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── client-server.test.ts │ │ │ ├── component-express-middleware.test.ts │ │ │ ├── component-koa-middleware.test.ts │ │ │ ├── counter.fixture.ts │ │ │ ├── http-client-server.test.ts │ │ │ ├── memory-navigator.test.ts │ │ │ ├── navigator.test.ts │ │ │ ├── storable.fixture.ts │ │ │ └── storable.test.ts │ │ └── tsconfig.json │ ├── memory-navigator/ │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ └── memory-navigator.ts │ │ └── tsconfig.json │ ├── memory-store/ │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ └── memory-store.ts │ │ └── tsconfig.json │ ├── mongodb-store/ │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ ├── mongodb-store.test.ts │ │ │ └── mongodb-store.ts │ │ └── tsconfig.json │ ├── navigator/ │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ ├── navigator.ts │ │ │ ├── utilities.test.ts │ │ │ └── utilities.ts │ │ └── tsconfig.json │ ├── observable/ │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ ├── observable.test.ts │ │ │ └── observable.ts │ │ └── tsconfig.json │ ├── react-integration/ │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── components.tsx │ │ │ ├── decorators.tsx │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ └── plugins.tsx │ │ └── tsconfig.json │ ├── routable/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── addressable.ts │ │ │ ├── decorators.test.ts │ │ │ ├── decorators.ts │ │ │ ├── index.ts │ │ │ ├── js-tests/ │ │ │ │ ├── decorators.test.js │ │ │ │ └── jsconfig.json │ │ │ ├── param.ts │ │ │ ├── pattern.ts │ │ │ ├── routable.test.ts │ │ │ ├── routable.ts │ │ │ ├── route.test.ts │ │ │ ├── route.ts │ │ │ ├── utilities.ts │ │ │ └── wrapper.ts │ │ └── tsconfig.json │ ├── storable/ │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── decorators.test.ts │ │ │ ├── decorators.ts │ │ │ ├── index-class.test.ts │ │ │ ├── index-class.ts │ │ │ ├── index.ts │ │ │ ├── js-tests/ │ │ │ │ ├── decorators.test.js │ │ │ │ └── jsconfig.json │ │ │ ├── operator.ts │ │ │ ├── properties/ │ │ │ │ ├── index.ts │ │ │ │ ├── storable-attribute.test.ts │ │ │ │ ├── storable-attribute.ts │ │ │ │ ├── storable-method.ts │ │ │ │ ├── storable-primary-identifier-attribute.ts │ │ │ │ ├── storable-property.ts │ │ │ │ └── storable-secondary-identifier-attribute.ts │ │ │ ├── query.ts │ │ │ ├── storable.ts │ │ │ ├── store-like.ts │ │ │ └── utilities.ts │ │ └── tsconfig.json │ ├── store/ │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── document.ts │ │ │ ├── expression.ts │ │ │ ├── index.ts │ │ │ ├── path.ts │ │ │ ├── store.test.ts │ │ │ ├── store.ts │ │ │ └── utilities.ts │ │ └── tsconfig.json │ ├── utilities/ │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── error.test.ts │ │ │ ├── error.ts │ │ │ ├── index.ts │ │ │ └── time.ts │ │ └── tsconfig.json │ └── with-roles/ │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src/ │ │ ├── decorators.test.ts │ │ ├── decorators.ts │ │ ├── index.ts │ │ ├── role.test.ts │ │ ├── role.ts │ │ ├── utilities.ts │ │ ├── with-roles.test.ts │ │ └── with-roles.ts │ └── tsconfig.json └── website/ ├── README.md ├── backend/ │ ├── boostr.config.mjs │ ├── boostr.config.private-template.mjs │ ├── package.json │ ├── src/ │ │ ├── components/ │ │ │ ├── application.ts │ │ │ ├── article.ts │ │ │ ├── entity.ts │ │ │ ├── newsletter.ts │ │ │ ├── user.ts │ │ │ └── with-author.ts │ │ ├── index.ts │ │ └── jwt.ts │ └── tsconfig.json ├── boostr.config.mjs ├── database/ │ ├── .gitignore │ ├── boostr.config.mjs │ └── boostr.config.private-template.mjs ├── frontend/ │ ├── boostr.config.mjs │ ├── package.json │ ├── public/ │ │ └── docs/ │ │ ├── v1/ │ │ │ ├── introduction/ │ │ │ │ ├── authorization-6eooNLZnaLZk4eJNHNGwUp-edited.immutable.md │ │ │ │ ├── data-storage-1YDg3LGZPF09H3opJUEDSp-edited.immutable.md │ │ │ │ ├── hello-world-7F76XnjXXBB7eeZTpEZwEX-edited.immutable.md │ │ │ │ ├── introduction-1NNtkbXN0zHvG6VR0jvPRK-edited.immutable.md │ │ │ │ ├── routing-4ubaWloMHuNNs0foagYUwi-edited.immutable.md │ │ │ │ └── web-app-5okryUyuriFFHXtFK9uZNY-edited.immutable.md │ │ │ └── reference/ │ │ │ ├── attribute-4P9dTivZ9HJ4Flr4Y8cTKv.immutable.md │ │ │ ├── attribute-selector-7FZWQpwLR7jpUHoBpvO9Et.immutable.md │ │ │ ├── aws-integration-3iPKbtjOS0uDgsZKk6Q0Kp.immutable.md │ │ │ ├── browser-router-6EIyPCgoPYsWmDmpyKMhce.immutable.md │ │ │ ├── component-6LvsJL2MA9RN6hmT0bBacd.immutable.md │ │ │ ├── component-client-1ISSjJNRL12I33addutRh4.immutable.md │ │ │ ├── component-express-middleware-6UieWJkEvLdXLYug4xuM16.immutable.md │ │ │ ├── component-http-client-6g8M9kRcBS4AQwFtAp7t9z.immutable.md │ │ │ ├── component-http-server-5VDR5fS2uD9iTkuHRSywjz.immutable.md │ │ │ ├── component-koa-middleware-402oMlpSmEIARQJLzn4qQg.immutable.md │ │ │ ├── component-server-e5RUuQXVcCIVLxHiNzCDO.immutable.md │ │ │ ├── embedded-component-4MiZBzspJQbUshU8Q93aJ8.immutable.md │ │ │ ├── identifier-attribute-6Jgrzmrlv4QJoBZVTYYDb9.immutable.md │ │ │ ├── identity-map-01fsjrVv6cSC6awmnNnn6c.immutable.md │ │ │ ├── index-1KeXILVBQNLQpRzBhIWjqB.immutable.md │ │ │ ├── memory-router-TGoBOhUXFPp3iDyKA9Sn0.immutable.md │ │ │ ├── memory-store-78uJRKSOEyr1WnXttCnd9u.immutable.md │ │ │ ├── method-3mc2TakviGDKFcdpxFaCIu.immutable.md │ │ │ ├── mongodb-store-1yTYtEyVy4ZLkF4A2QFaAh.immutable.md │ │ │ ├── observable-6JJJVlFHPe2kPQ4NhsJg7O.immutable.md │ │ │ ├── primary-identifier-attribute-6qTkOuHNN4Bq3EjC9JGVqr.immutable.md │ │ │ ├── property-5bbULbxC52tIPaMAcPEp27.immutable.md │ │ │ ├── query-Q04JuSZAcx8HGvYktmTlI.immutable.md │ │ │ ├── react-integration-3EgAB99IhnithzdqiytvwF.immutable.md │ │ │ ├── role-UodTLJi6Lg6UPXOKZdp8K.immutable.md │ │ │ ├── routable-3zt4vMLvzQR3aqOwoWd3xV.immutable.md │ │ │ ├── route-1rS9AS4eda7qe7lWTU4WKF.immutable.md │ │ │ ├── router-5zhzJL9MYM1yaHY6CXlkso.immutable.md │ │ │ ├── secondary-identifier-attribute-7BqK9EmnCMCHIAroYiQDth.immutable.md │ │ │ ├── storable-2maAYPX5kWufBfVolYVgdc.immutable.md │ │ │ ├── storable-attribute-7MiyqvA7PUf2E2YeoqA912.immutable.md │ │ │ ├── storable-method-1rnW3aKFlFYZ3kIJdCGXRv.immutable.md │ │ │ ├── storable-primary-identifier-attribute-74IwGTycdA7NVAybBly3t1.immutable.md │ │ │ ├── storable-property-5LuWR7uuQ1qdebK3iszZJO.immutable.md │ │ │ ├── storable-secondary-identifier-attribute-6CqhR0kLxoLhtWU85EcsZU.immutable.md │ │ │ ├── store-6BvlCSWkAb7smHsvoIGjv1.immutable.md │ │ │ ├── validator-2strlvnapeTZmbWT3o3VM1.immutable.md │ │ │ ├── value-type-23f3WjnZNDfFejs5ceJVcY.immutable.md │ │ │ └── with-roles-2PYi0037F3Mxg80b5YKb26.immutable.md │ │ └── v2/ │ │ ├── concepts/ │ │ │ └── coming-soon-38q4cuyCWekHuz36oHyMKG.immutable.md │ │ ├── guides/ │ │ │ └── coming-soon-6TWZbDxh3EVysir57k1tga.immutable.md │ │ ├── introduction/ │ │ │ ├── handling-authorization-1sLAebuD81FLlSvJbAuJmZ.immutable.md │ │ │ ├── hello-world/ │ │ │ │ └── hello-world-2FHBCgFRhZX9qH9O1RkTlZ.immutable.md │ │ │ ├── introduction-1iLUBREWe2BB65qw9G4uzm.immutable.md │ │ │ └── storing-data-40jzWiVleR8ZE0fqfGTbO2.immutable.md │ │ └── reference/ │ │ ├── addressable-6n5npS1cCoPKLkFrYU0Pk7.immutable.md │ │ ├── attribute-4IRansPGIm4E3gunH3Ztlq.immutable.md │ │ ├── attribute-selector-76ksImknctJspHLg12sRpR.immutable.md │ │ ├── aws-integration-3Rt2txpqioYQsmijgSqqwF.immutable.md │ │ ├── browser-navigator-eYIxYKNFdRYiKTxIWAjqd.immutable.md │ │ ├── component-1zBrZVglO2cjDP9aO87pOR.immutable.md │ │ ├── component-client-69HLk6gKzg5DfWcVC2bQWS.immutable.md │ │ ├── component-express-middleware-7KGBerWY9jB9r9eOjwLmPq.immutable.md │ │ ├── component-http-client-2rHYFMcsawI16EvGk8372w.immutable.md │ │ ├── component-http-server-5aWTLSzVvm0WohfP6af7OP.immutable.md │ │ ├── component-koa-middleware-6aVaQsSgkzWt35XyqSMuqx.immutable.md │ │ ├── component-server-i0AsOVFbGCJUrJ9mJyjPh.immutable.md │ │ ├── embedded-component-3YdGqfLNxl8c001AvWxHZ6.immutable.md │ │ ├── identifier-attribute-2w8hckqCKGHPurgIGY5vhT.immutable.md │ │ ├── identity-map-7sLdA5rsLvflf8T8OsZfGO.immutable.md │ │ ├── index-1Uz5JX1XB7V67nfM6tvnSV.immutable.md │ │ ├── memory-navigator-5wC9TEZn2cen7LuV4cGPOj.immutable.md │ │ ├── memory-store-3QHbAhGolblHx4OY17rEP.immutable.md │ │ ├── method-MAFTDipVXsOXj4o2CJLNQ.immutable.md │ │ ├── mongodb-store-37qsYI8A5ctkFlHTDVIc3O.immutable.md │ │ ├── navigator-5vNC9hp62PFUvxh0PLQcJ9.immutable.md │ │ ├── observable-7alzx5bqV1gD2Aj60hWQ0L.immutable.md │ │ ├── primary-identifier-attribute-5EqFynrkNUTa9DxYYmbmOl.immutable.md │ │ ├── property-bs5Xm31fCQXQkoZL24prZ.immutable.md │ │ ├── query-146gEiaHLp06V3LqhxRCzC.immutable.md │ │ ├── react-integration-4BgvR6gvTuqLdpNymyUYS1.immutable.md │ │ ├── role-3zJ3w6whE8RucN9kAhS5Hv.immutable.md │ │ ├── routable-7zjaksbkNa8sbNfqNnEKUu.immutable.md │ │ ├── route-3O1kMMGl4yZc9LGxH38Z3G.immutable.md │ │ ├── sanitizer-3hc34yGhzbK1cupoMtP6Dv.immutable.md │ │ ├── secondary-identifier-attribute-27lBtqHBopmgIAWwmeoLBg.immutable.md │ │ ├── storable-3UMrfeCfFVaVttjxtmCvVy.immutable.md │ │ ├── storable-attribute-1EYIvaUK2WvHmV3OxG5IzJ.immutable.md │ │ ├── storable-method-1o1egWUfiZzxO6y7Sda4rb.immutable.md │ │ ├── storable-primary-identifier-attribute-7qq06XjPnNRXumxcngJvZv.immutable.md │ │ ├── storable-property-6Ooq1wLy5pGx7Jb1qRrRyf.immutable.md │ │ ├── storable-secondary-identifier-attribute-1TKIxZX9JZ2aGFU5iEmUSv.immutable.md │ │ ├── store-1mGBVGCiO4K5X9dBakik9Z.immutable.md │ │ ├── validator-2aTKblcN30ADdXqrI3bHXv.immutable.md │ │ ├── value-type-FqKdGxNpEPIDVR56kq4NN.immutable.md │ │ ├── with-roles-CcLPnrWKVYxnZXgNTqVYi.immutable.md │ │ └── wrapper-4uhJFGS18pbZTt9qdtLnL2.immutable.md │ ├── src/ │ │ ├── components/ │ │ │ ├── application.tsx │ │ │ ├── article.tsx │ │ │ ├── blog.tsx │ │ │ ├── docs.tsx │ │ │ ├── home.tsx │ │ │ ├── newsletter.tsx │ │ │ └── user.tsx │ │ ├── custom.d.ts │ │ ├── docs.json │ │ ├── index.ts │ │ ├── markdown.tsx │ │ ├── styles.ts │ │ └── ui.tsx │ └── tsconfig.json ├── package.json └── redirection/ ├── package.json ├── public/ │ └── index.html └── simple-deployment.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # editorconfig.org root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.md] trim_trailing_whitespace = false ================================================ FILE: .gitignore ================================================ .DS_Store *.log node_modules .npmrc package-lock.json dist build _private *.private.* ================================================ FILE: .npm-dev-registry.json ================================================ { "packages": ["./packages/*"] } ================================================ FILE: .prettierignore ================================================ node_modules dist build ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": ["esbenp.prettier-vscode", "editorconfig.editorconfig"] } ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: - Demonstrating empathy and kindness toward other people - Being respectful of differing opinions, viewpoints, and experiences - Giving and gracefully accepting constructive feedback - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience - Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: - The use of sexualized language or imagery, and sexual attention or advances of any kind - Trolling, insulting or derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or email address, without their explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at hello@layrjs.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Manuel Vila Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

Layr

> Dramatically simplify full‑stack development. ## Documentation Check out the [documentation](https://layrjs.com/docs) for a quick introduction and a comprehensive API description. ## Roadmap ### Components - [x] Basic components - [x] Controlled attributes - [x] Component provision - [x] Cross-layer inheritance - [x] Remote method invocation - [x] Optimized serialization - [ ] Weak Identity Map - [ ] Component subscriptions (realtime updates) - [ ] HTTP Caching ### Storage - [x] Basic storage (MongoDB) - [x] Indexes - [x] Identifier attributes - [x] Regular attributes - [x] Compound attributes - [x] Referenced components - [x] Embedded components - [ ] Automatic migrations - [x] Indexes - [ ] Default values - [ ] Renamed components - [ ] Renamed attributes - [ ] Custom migrations - [ ] Polymorphism - [ ] Transactions - [ ] Sugar to query reverse relationships - [ ] Query subscriptions (realtime updates) - [ ] Support for more databases (PostgreSQL, MySQL, DynamoDB,...) ### Routing - [x] Basic routing - [x] Nested routing ### Authorizations - [x] Basic authorizations - [x] Role-based authorizations ### Integrations - [x] React integration - [x] Basic AWS integration ## Contributing Contributions are welcome. Before 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. To 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). ## License MIT ================================================ FILE: assets/frontend-backend.excalidraw ================================================ { "type": "excalidraw", "version": 2, "source": "https://excalidraw.com", "elements": [ { "id": "qmZPyH3vxEhjWDCfuvfXG", "type": "rectangle", "x": 473.40625, "y": -192.14453125, "width": 859.4531250000005, "height": 478.06640624999994, "angle": 0, "strokeColor": "#eceff1", "backgroundColor": "#263238", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "sharp", "seed": 1020737077, "version": 490, "versionNonce": 552337013, "isDeleted": false, "boundElementIds": null }, { "type": "text", "version": 868, "versionNonce": 850464603, "isDeleted": false, "id": "pZqWf7TeaoVvLCg8jN1fw", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 629.6852678571429, "y": -62.34207589285717, "strokeColor": "#ff4081", "backgroundColor": "transparent", "width": 110, "height": 35, "seed": 951595797, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Classes", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "id": "tzB0MO8qggda-Bt_Wm_TB", "type": "ellipse", "x": 518.53125, "y": -91.59375, "width": 328.45703124999994, "height": 328.45703124999994, "angle": 0, "strokeColor": "#ff4081", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "sharp", "seed": 1145563989, "version": 426, "versionNonce": 1479436245, "isDeleted": false, "boundElementIds": [ "mO83jV49wubJCif3eCFF7" ] }, { "type": "text", "version": 903, "versionNonce": 1445751803, "isDeleted": false, "id": "3XcH3jOWUovwndwQ5OWNZ", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 613.9001116071429, "y": -8.635044642857167, "strokeColor": "#ff4081", "backgroundColor": "transparent", "width": 135, "height": 35, "seed": 1510068853, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Instances", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 1064, "versionNonce": 1109173557, "isDeleted": false, "id": "jfBVn4_IY4-6kYgH4CLc2", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 541.4782366071429, "y": 44.08370535714283, "strokeColor": "#ff4081", "backgroundColor": "transparent", "width": 145, "height": 35, "seed": 1265264027, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Attributes", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 1171, "versionNonce": 2001941659, "isDeleted": false, "id": "pbft6g1zEaKzjxE9qI4WH", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 713.2165178571429, "y": 43.12667410714283, "strokeColor": "#ff4081", "backgroundColor": "transparent", "width": 109, "height": 35, "seed": 1694556846, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Methods", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 1101, "versionNonce": 1109145237, "isDeleted": false, "id": "Ml8ehnX8s0BLj1BCSxw5v", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 602.6696428571429, "y": 101.35714285714283, "strokeColor": "#ff4081", "backgroundColor": "transparent", "width": 148, "height": 35, "seed": 479631835, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "References", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 1129, "versionNonce": 634906939, "isDeleted": false, "id": "Lmij_syGf4Gxc0oIXhstt", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 608.6813616071429, "y": 157.23995535714283, "strokeColor": "#ff4081", "backgroundColor": "transparent", "width": 148, "height": 35, "seed": 622730715, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Exceptions", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 886, "versionNonce": 853711861, "isDeleted": false, "id": "g4v8TXu9aqJoc76AjvU5E", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 1073.730189732143, "y": -65.31277901785715, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 110, "height": 35, "seed": 292264597, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Classes", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "id": "XS2Qi1--uR3zLQP7iXcp9", "type": "ellipse", "x": 962.576171875, "y": -92.56445312499999, "width": 328.45703124999994, "height": 328.45703124999994, "angle": 0, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "sharp", "seed": 1084278075, "version": 415, "versionNonce": 1461520859, "isDeleted": false, "boundElementIds": null }, { "type": "text", "version": 923, "versionNonce": 2088064341, "isDeleted": false, "id": "2VgLJ7nXt0QOFYIBEIWAa", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 1057.945033482143, "y": -13.605747767857153, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 135, "height": 35, "seed": 715489269, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Instances", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 1083, "versionNonce": 808785531, "isDeleted": false, "id": "rOfltch3LXDrC9hL2pwbP", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 985.3278459821429, "y": 45.11300223214285, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 145, "height": 35, "seed": 1550476763, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Attributes", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 1190, "versionNonce": 1781129909, "isDeleted": false, "id": "woGF69RnnYqW_PttWeNeI", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 1157.066127232143, "y": 44.15597098214285, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 109, "height": 35, "seed": 1111210325, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Methods", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 1116, "versionNonce": 266506011, "isDeleted": false, "id": "vJWV-kfqxj7Mk8eWoWdTi", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 1046.714564732143, "y": 104.45284598214283, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 148, "height": 35, "seed": 1534521979, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "References", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 1141, "versionNonce": 1084605461, "isDeleted": false, "id": "puaE8IFTDuN6P4YNxaFcX", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 1052.726283482143, "y": 164.26925223214283, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 148, "height": 35, "seed": 1904461493, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Exceptions", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "id": "6iVlZb2OSireaYoyJCTfe", "type": "line", "x": 872.43359375, "y": 74.57421875, "width": 60.234375, "height": 0.82421875, "angle": 0, "strokeColor": "#eceff1", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", "seed": 298196123, "version": 294, "versionNonce": 1486353339, "isDeleted": false, "boundElementIds": null, "points": [ [ 0, 0 ], [ 60.234375, -0.82421875 ] ], "lastCommittedPoint": null, "startBinding": null, "endBinding": null }, { "id": "DKp1L9tGICjQBmD7WviKE", "type": "line", "x": 924, "y": 63.85156250000006, "width": 14.00390625, "height": 10.234375, "angle": 0, "strokeColor": "#eceff1", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", "seed": 1899926395, "version": 295, "versionNonce": 333347189, "isDeleted": false, "boundElementIds": null, "points": [ [ 0, 0 ], [ 14.00390625, 10.234375 ] ], "lastCommittedPoint": null, "startBinding": null, "endBinding": null }, { "id": "2KOhHgB2-vgVjCIrzDoFC", "type": "line", "x": 938.40234375, "y": 73.69140625, "width": 19.35546875, "height": 14.65234375, "angle": 0, "strokeColor": "#eceff1", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", "seed": 1385976021, "version": 289, "versionNonce": 2054669403, "isDeleted": false, "boundElementIds": null, "points": [ [ 0, 0 ], [ -19.35546875, 14.65234375 ] ], "lastCommittedPoint": null, "startBinding": null, "endBinding": null }, { "id": "aMvlrbJ4JhavinX6IuIip", "type": "line", "x": 871.75, "y": 72.01171875, "width": 15.390625, "height": 8.05859375, "angle": 0, "strokeColor": "#eceff1", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", "seed": 129732827, "version": 331, "versionNonce": 1561952981, "isDeleted": false, "boundElementIds": null, "points": [ [ 0, 0 ], [ 15.390625, -8.05859375 ] ], "lastCommittedPoint": null, "startBinding": null, "endBinding": null }, { "id": "Heg0L7qNczN3uKd2rUc4H", "type": "line", "x": 869.2109375, "y": 74.93359375, "width": 20.64453125, "height": 12.38671875, "angle": 0, "strokeColor": "#eceff1", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", "seed": 592936059, "version": 337, "versionNonce": 1028948219, "isDeleted": false, "boundElementIds": null, "points": [ [ 0, 0 ], [ 20.64453125, 12.38671875 ] ], "lastCommittedPoint": null, "startBinding": null, "endBinding": null }, { "type": "text", "version": 1066, "versionNonce": 1357668405, "isDeleted": false, "id": "tGfMAIBrPStcxnXBLGc8q", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 605.6969866071429, "y": -156.91238839285717, "strokeColor": "#eceff1", "backgroundColor": "transparent", "width": 152, "height": 45, "seed": 704809819, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 36, "fontFamily": 1, "text": "Frontend", "baseline": 32, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 1013, "versionNonce": 1253091739, "isDeleted": false, "id": "-eYtt3kyzRMtMkKUqbNJ1", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 1053.138392857143, "y": -156.91238839285717, "strokeColor": "#eceff1", "backgroundColor": "transparent", "width": 145, "height": 45, "seed": 553189045, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 36, "fontFamily": 1, "text": "Backend", "baseline": 32, "textAlign": "center", "verticalAlign": "top" } ], "appState": { "viewBackgroundColor": "#000", "gridSize": null } } ================================================ FILE: assets/frontend-webapi-backend.excalidraw ================================================ { "type": "excalidraw", "version": 2, "source": "https://excalidraw.com", "elements": [ { "id": "qmZPyH3vxEhjWDCfuvfXG", "type": "rectangle", "x": 284.2421875, "y": -189.6875, "width": 1237.3945312500002, "height": 478.06640624999994, "angle": 0, "strokeColor": "#eceff1", "backgroundColor": "#263238", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "sharp", "seed": 1020737077, "version": 359, "versionNonce": 815580763, "isDeleted": false, "boundElementIds": null }, { "type": "text", "version": 677, "versionNonce": 1952969915, "isDeleted": false, "id": "pZqWf7TeaoVvLCg8jN1fw", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 439.0641741071429, "y": -59.88504464285717, "strokeColor": "#ff4081", "backgroundColor": "transparent", "width": 110, "height": 35, "seed": 951595797, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Classes", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "id": "tzB0MO8qggda-Bt_Wm_TB", "type": "ellipse", "x": 327.91015625, "y": -89.13671875, "width": 328.45703124999994, "height": 328.45703124999994, "angle": 0, "strokeColor": "#ff4081", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "sharp", "seed": 1145563989, "version": 235, "versionNonce": 768854261, "isDeleted": false, "boundElementIds": [ "mO83jV49wubJCif3eCFF7" ] }, { "type": "text", "version": 712, "versionNonce": 1555921717, "isDeleted": false, "id": "3XcH3jOWUovwndwQ5OWNZ", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 423.2790178571429, "y": -6.178013392857167, "strokeColor": "#ff4081", "backgroundColor": "transparent", "width": 135, "height": 35, "seed": 1510068853, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Instances", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 873, "versionNonce": 1655329909, "isDeleted": false, "id": "jfBVn4_IY4-6kYgH4CLc2", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 350.8571428571429, "y": 46.54073660714283, "strokeColor": "#ff4081", "backgroundColor": "transparent", "width": 145, "height": 35, "seed": 1265264027, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Attributes", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 980, "versionNonce": 858462555, "isDeleted": false, "id": "pbft6g1zEaKzjxE9qI4WH", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 522.5954241071429, "y": 45.58370535714283, "strokeColor": "#ff4081", "backgroundColor": "transparent", "width": 109, "height": 35, "seed": 1694556846, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Methods", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 910, "versionNonce": 2116520635, "isDeleted": false, "id": "Ml8ehnX8s0BLj1BCSxw5v", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 412.0485491071429, "y": 103.81417410714283, "strokeColor": "#ff4081", "backgroundColor": "transparent", "width": 148, "height": 35, "seed": 479631835, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "References", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 938, "versionNonce": 402422619, "isDeleted": false, "id": "Lmij_syGf4Gxc0oIXhstt", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 418.0602678571429, "y": 159.69698660714283, "strokeColor": "#ff4081", "backgroundColor": "transparent", "width": 148, "height": 35, "seed": 622730715, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Exceptions", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 855, "versionNonce": 776023291, "isDeleted": false, "id": "g4v8TXu9aqJoc76AjvU5E", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 1262.507533482143, "y": -62.85574776785715, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 110, "height": 35, "seed": 292264597, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Classes", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "id": "XS2Qi1--uR3zLQP7iXcp9", "type": "ellipse", "x": 1151.353515625, "y": -90.10742187499999, "width": 328.45703124999994, "height": 328.45703124999994, "angle": 0, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "sharp", "seed": 1084278075, "version": 384, "versionNonce": 863007797, "isDeleted": false, "boundElementIds": null }, { "type": "text", "version": 892, "versionNonce": 1361565083, "isDeleted": false, "id": "2VgLJ7nXt0QOFYIBEIWAa", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 1246.722377232143, "y": -11.148716517857153, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 135, "height": 35, "seed": 715489269, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Instances", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 1052, "versionNonce": 291950453, "isDeleted": false, "id": "rOfltch3LXDrC9hL2pwbP", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 1174.105189732143, "y": 47.57003348214285, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 145, "height": 35, "seed": 1550476763, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Attributes", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 1159, "versionNonce": 952618587, "isDeleted": false, "id": "woGF69RnnYqW_PttWeNeI", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 1345.843470982143, "y": 46.61300223214285, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 109, "height": 35, "seed": 1111210325, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Methods", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 1085, "versionNonce": 785296117, "isDeleted": false, "id": "vJWV-kfqxj7Mk8eWoWdTi", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 1235.491908482143, "y": 106.90987723214283, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 148, "height": 35, "seed": 1534521979, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "References", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 1110, "versionNonce": 1794992859, "isDeleted": false, "id": "puaE8IFTDuN6P4YNxaFcX", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 1241.503627232143, "y": 166.72628348214283, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 148, "height": 35, "seed": 1904461493, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Exceptions", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "id": "KHG3bd42lmlGY_6htw4yh", "type": "diamond", "x": 767.68359375, "y": -87.37890625, "width": 276.44140625000006, "height": 326.80859375, "angle": 0, "strokeColor": "#aeea00", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "sharp", "seed": 1156814363, "version": 679, "versionNonce": 158888565, "isDeleted": false, "boundElementIds": null }, { "type": "text", "version": 828, "versionNonce": 816995989, "isDeleted": false, "id": "PTJtBBiRYU7sirmpKXaaa", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 876.2438616071429, "y": -29.291294642857167, "strokeColor": "#aeea00", "backgroundColor": "transparent", "width": 53, "height": 35, "seed": 1015869717, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Get", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 896, "versionNonce": 823673147, "isDeleted": false, "id": "Ui7ygay9V1Gwb_JPUnSoZ", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 835.4743303571429, "y": 21.189174107142833, "strokeColor": "#aeea00", "backgroundColor": "transparent", "width": 64, "height": 35, "seed": 2081802805, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Post", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 1114, "versionNonce": 1014219765, "isDeleted": false, "id": "mCXJ_OIM1l6uuO5aerbPl", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 808.4547991071429, "y": 74.62667410714283, "strokeColor": "#aeea00", "backgroundColor": "transparent", "width": 96, "height": 35, "seed": 872123483, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Delete", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 950, "versionNonce": 1217552859, "isDeleted": false, "id": "PvsMVeFX5yNreGLLR6cZl", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 929.6696428571429, "y": 23.579799107142833, "strokeColor": "#aeea00", "backgroundColor": "transparent", "width": 48, "height": 35, "seed": 89552757, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Put", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 945, "versionNonce": 161578325, "isDeleted": false, "id": "wWagJGH0s7sy5ZggcAPSU", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 931.4508928571429, "y": 75.54464285714283, "strokeColor": "#aeea00", "backgroundColor": "transparent", "width": 67, "height": 35, "seed": 1694554683, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "URLs", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 978, "versionNonce": 1301642165, "isDeleted": false, "id": "5uBcTnXeNlcF037DuFLt8", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 864.4860491071429, "y": 130.73604910714283, "strokeColor": "#aeea00", "backgroundColor": "transparent", "width": 76, "height": 35, "seed": 989301691, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "JSON", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "id": "6iVlZb2OSireaYoyJCTfe", "type": "line", "x": 681.8125, "y": 77.03125, "width": 60.234375, "height": 0.82421875, "angle": 0, "strokeColor": "#eceff1", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", "seed": 298196123, "version": 103, "versionNonce": 1469817205, "isDeleted": false, "boundElementIds": null, "points": [ [ 0, 0 ], [ 60.234375, -0.82421875 ] ], "lastCommittedPoint": null, "startBinding": null, "endBinding": null }, { "id": "DKp1L9tGICjQBmD7WviKE", "type": "line", "x": 733.37890625, "y": 66.30859375000006, "width": 14.00390625, "height": 10.234375, "angle": 0, "strokeColor": "#eceff1", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", "seed": 1899926395, "version": 104, "versionNonce": 981539931, "isDeleted": false, "boundElementIds": null, "points": [ [ 0, 0 ], [ 14.00390625, 10.234375 ] ], "lastCommittedPoint": null, "startBinding": null, "endBinding": null }, { "id": "2KOhHgB2-vgVjCIrzDoFC", "type": "line", "x": 747.78125, "y": 76.1484375, "width": 19.35546875, "height": 14.65234375, "angle": 0, "strokeColor": "#eceff1", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", "seed": 1385976021, "version": 98, "versionNonce": 2129767125, "isDeleted": false, "boundElementIds": null, "points": [ [ 0, 0 ], [ -19.35546875, 14.65234375 ] ], "lastCommittedPoint": null, "startBinding": null, "endBinding": null }, { "id": "aMvlrbJ4JhavinX6IuIip", "type": "line", "x": 681.12890625, "y": 74.46875, "width": 15.390625, "height": 8.05859375, "angle": 0, "strokeColor": "#eceff1", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", "seed": 129732827, "version": 140, "versionNonce": 1651693819, "isDeleted": false, "boundElementIds": null, "points": [ [ 0, 0 ], [ 15.390625, -8.05859375 ] ], "lastCommittedPoint": null, "startBinding": null, "endBinding": null }, { "id": "Heg0L7qNczN3uKd2rUc4H", "type": "line", "x": 678.58984375, "y": 77.390625, "width": 20.64453125, "height": 12.38671875, "angle": 0, "strokeColor": "#eceff1", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", "seed": 592936059, "version": 146, "versionNonce": 1434421301, "isDeleted": false, "boundElementIds": null, "points": [ [ 0, 0 ], [ 20.64453125, 12.38671875 ] ], "lastCommittedPoint": null, "startBinding": null, "endBinding": null }, { "id": "MziFM0bqQkfptvWC6js9V", "type": "line", "x": 1065.697265625, "y": 81.41796874999997, "width": 60.234375, "height": 0.82421875, "angle": 0, "strokeColor": "#eceff1", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", "seed": 194372437, "version": 241, "versionNonce": 1782841755, "isDeleted": false, "boundElementIds": null, "points": [ [ 0, 0 ], [ 60.234375, -0.82421875 ] ], "lastCommittedPoint": null, "startBinding": null, "endBinding": null }, { "id": "1K59ruDxOFEs38bKS3wOR", "type": "line", "x": 1117.263671875, "y": 70.69531250000003, "width": 14.00390625, "height": 10.234375, "angle": 0, "strokeColor": "#eceff1", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", "seed": 195848315, "version": 248, "versionNonce": 1294645653, "isDeleted": false, "boundElementIds": null, "points": [ [ 0, 0 ], [ 14.00390625, 10.234375 ] ], "lastCommittedPoint": null, "startBinding": null, "endBinding": null }, { "id": "HsDXwOGJ1sjxaRJXDDZvp", "type": "line", "x": 1131.666015625, "y": 80.53515624999997, "width": 19.35546875, "height": 14.65234375, "angle": 0, "strokeColor": "#eceff1", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", "seed": 570994869, "version": 236, "versionNonce": 2066159163, "isDeleted": false, "boundElementIds": null, "points": [ [ 0, 0 ], [ -19.35546875, 14.65234375 ] ], "lastCommittedPoint": null, "startBinding": null, "endBinding": null }, { "id": "0pX2lORYW_GourTAVS2dY", "type": "line", "x": 1065.013671875, "y": 78.85546874999997, "width": 15.390625, "height": 8.05859375, "angle": 0, "strokeColor": "#eceff1", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", "seed": 1688183067, "version": 278, "versionNonce": 2133376757, "isDeleted": false, "boundElementIds": null, "points": [ [ 0, 0 ], [ 15.390625, -8.05859375 ] ], "lastCommittedPoint": null, "startBinding": null, "endBinding": null }, { "id": "V7OyGddYwmngOBKRWiOir", "type": "line", "x": 1062.474609375, "y": 81.77734374999997, "width": 20.64453125, "height": 12.38671875, "angle": 0, "strokeColor": "#eceff1", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", "seed": 1870320149, "version": 284, "versionNonce": 1946064603, "isDeleted": false, "boundElementIds": null, "points": [ [ 0, 0 ], [ 20.64453125, 12.38671875 ] ], "lastCommittedPoint": null, "startBinding": null, "endBinding": null }, { "type": "text", "version": 875, "versionNonce": 1438821333, "isDeleted": false, "id": "tGfMAIBrPStcxnXBLGc8q", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 415.0758928571429, "y": -154.45535714285717, "strokeColor": "#eceff1", "backgroundColor": "transparent", "width": 152, "height": 45, "seed": 704809819, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 36, "fontFamily": 1, "text": "Frontend", "baseline": 32, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 932, "versionNonce": 1294663739, "isDeleted": false, "id": "D4N_3rWoMa0_PDwOF4QG4", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 829.7399553571429, "y": -150.45535714285717, "strokeColor": "#eceff1", "backgroundColor": "transparent", "width": 149, "height": 45, "seed": 1200951285, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 36, "fontFamily": 1, "text": "Web API", "baseline": 32, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 982, "versionNonce": 1744068155, "isDeleted": false, "id": "-eYtt3kyzRMtMkKUqbNJ1", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 1241.915736607143, "y": -154.45535714285717, "strokeColor": "#eceff1", "backgroundColor": "transparent", "width": 145, "height": 45, "seed": 553189045, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 36, "fontFamily": 1, "text": "Backend", "baseline": 32, "textAlign": "center", "verticalAlign": "top" } ], "appState": { "viewBackgroundColor": "#000", "gridSize": null } } ================================================ FILE: assets/typical-stack-vs-layr-stack-light-mode.excalidraw ================================================ { "type": "excalidraw", "version": 2, "source": "https://excalidraw.com", "elements": [ { "type": "rectangle", "version": 518, "versionNonce": 1142497202, "isDeleted": false, "id": "78pcPSx_TrkBrqMvVMUaS", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 314.42578125, "y": -47.8984375, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 250.83482142857142, "height": 67.28515625, "seed": 132882798, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [] }, { "type": "text", "version": 203, "versionNonce": 2102357102, "isDeleted": false, "id": "Tg35xQQF7BB9-_T5IlAPi", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 345.87890625, "y": -31.755859375, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 187, "height": 35, "seed": 74600110, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Data Access", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "rectangle", "version": 722, "versionNonce": 1392400754, "isDeleted": false, "id": "isBhZDRkcIoWGbySBsLEy", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 312.97265625, "y": 51.353515625, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 252.4587053571429, "height": 67.28515625, "seed": 298779118, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [] }, { "type": "text", "version": 403, "versionNonce": 633581230, "isDeleted": false, "id": "j7kpflnVnuaDP-1F5MCcc", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 340.42578125, "y": 67.49609375, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 199, "height": 35, "seed": 11834414, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Backend Model", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "rectangle", "version": 812, "versionNonce": 1284922162, "isDeleted": false, "id": "zk1Tzfs7TN7NuvV9nUrkK", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 312.97265625, "y": 150.568359375, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 253.4743303571429, "height": 67.28515625, "seed": 1135815662, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [ "_q_pfShbWagNPeqr2Cw4q", "CzbKjuh5LlBF8vEsdGoYs", "euFzlspcyBdei7ZjDGsTE" ] }, { "type": "text", "version": 429, "versionNonce": 854960366, "isDeleted": false, "id": "pbft6g1zEaKzjxE9qI4WH", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 366.1735491071429, "y": 166.48214285714283, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 152, "height": 35, "seed": 1694556846, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "API Server", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "rectangle", "version": 837, "versionNonce": 1598563570, "isDeleted": false, "id": "G_sH-_BODgjExxHFIJUO7", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 313.0546875, "y": 251.623046875, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 251.515625, "height": 67.28515625, "seed": 2087476462, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [ "P-5Se_P31c9YU_W_jiyRL", "CzbKjuh5LlBF8vEsdGoYs" ] }, { "type": "text", "version": 467, "versionNonce": 879647534, "isDeleted": false, "id": "FWR6GQyMu_VgSTbgtdktG", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 369.21205357142856, "y": 268.20647321428567, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 144, "height": 35, "seed": 82743086, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "API Client", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "rectangle", "version": 813, "versionNonce": 1419425458, "isDeleted": false, "id": "RxupO9IzGSd0WgL0ECubS", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 312.97265625, "y": 355.587890625, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 253.07812500000003, "height": 67.28515625, "seed": 290532466, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [ "GNYre13yKH_YUl3IHWLJV", "f72O2kwJb9RVWZ6WUwfn-", "tcnX3h7U3Y7nCrYotIHRV", "P-5Se_P31c9YU_W_jiyRL" ] }, { "type": "text", "version": 516, "versionNonce": 1620493678, "isDeleted": false, "id": "UJ8QgwFB2cterDA5_P1XR", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 337.83761160714283, "y": 370.9436383928572, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 205, "height": 35, "seed": 2132201010, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [ "uGtob-oyCnRKt3cM6Pyg_" ], "fontSize": 28, "fontFamily": 1, "text": "Frontend Model", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "rectangle", "version": 900, "versionNonce": 758452338, "isDeleted": false, "id": "I5x5Y1aUFonRrUCGi6_jw", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 312.97265625, "y": 458.373046875, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 252.57421875000006, "height": 67.28515625, "seed": 1935830894, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [ "GNYre13yKH_YUl3IHWLJV", "f72O2kwJb9RVWZ6WUwfn-", "qDACPeHQipsh0zAP-atrl" ] }, { "type": "text", "version": 591, "versionNonce": 951576494, "isDeleted": false, "id": "Ozy5i1Z2EEzX3kAVJ5IY9", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 335.1579241071429, "y": 474.50446428571445, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 207, "height": 35, "seed": 1037108850, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [ "uGtob-oyCnRKt3cM6Pyg_" ], "fontSize": 28, "fontFamily": 1, "text": "User Interface", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "rectangle", "version": 1078, "versionNonce": 1083733508, "isDeleted": false, "id": "BK5gVfRenINsXjknIgSxf", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "dotted", "roughness": 1, "opacity": 100, "angle": 0, "x": 695.0412946428572, "y": 153.30106026785717, "strokeColor": "#ff4081", "backgroundColor": "transparent", "width": 195.533482142857, "height": 67.28515625, "seed": 544079922, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [ "xQNTkIrt5bvAXhVmKvBxM", "hTkLQGX8fKKkms2V_vm0r", "CzbKjuh5LlBF8vEsdGoYs", "mnKhSKUhk7EFCekLtz1fQ" ] }, { "type": "text", "version": 787, "versionNonce": 198499388, "isDeleted": false, "id": "iQoD1G_MsPZm4xfuufUxe", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 734.5055803571429, "y": 168.5563616071429, "strokeColor": "#ff4081", "backgroundColor": "transparent", "width": 113, "height": 35, "seed": 48441842, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Backend", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "rectangle", "version": 1220, "versionNonce": 1839404420, "isDeleted": false, "id": "pCkj2Qaeh9PDMAzSCWPM9", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "dotted", "roughness": 1, "opacity": 100, "angle": 0, "x": 694.9520089285714, "y": 252.49637276785717, "strokeColor": "#ff4081", "backgroundColor": "transparent", "width": 193.7477678571429, "height": 67.28515625, "seed": 374338158, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [ "xQNTkIrt5bvAXhVmKvBxM", "hTkLQGX8fKKkms2V_vm0r", "mnKhSKUhk7EFCekLtz1fQ" ] }, { "type": "text", "version": 810, "versionNonce": 31847612, "isDeleted": false, "id": "-CvZWfngqmHMBLZxpCqri", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 732.0145089285714, "y": 269.67131696428584, "strokeColor": "#ff4081", "backgroundColor": "transparent", "width": 119, "height": 35, "seed": 415462574, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Frontend", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "rectangle", "version": 1501, "versionNonce": 1865270532, "isDeleted": false, "id": "MSfRFHL5NGAG-wZc7hZub", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 664.8816964285714, "y": 117.36356026785717, "strokeColor": "#ff4081", "backgroundColor": "transparent", "width": 252.3643973214285, "height": 231.47656249999994, "seed": 153297006, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [ "CzbKjuh5LlBF8vEsdGoYs", "euFzlspcyBdei7ZjDGsTE" ] }, { "type": "text", "version": 347, "versionNonce": 1710969860, "isDeleted": false, "id": "HblX02yOmNDLcQXmIx14_", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 318.7762276785714, "y": -124.638671875, "strokeColor": "#607d8b", "backgroundColor": "transparent", "width": 244, "height": 45, "seed": 1398208946, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 36, "fontFamily": 1, "text": "Typical Stack", "baseline": 32, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 666, "versionNonce": 152493628, "isDeleted": false, "id": "oi9Jo0SrqAcTWeuWmDnFT", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 670.4157366071428, "y": 42.34793526785717, "strokeColor": "#607d8b", "backgroundColor": "transparent", "width": 241, "height": 45, "seed": 1175668722, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 36, "fontFamily": 1, "text": "Layr Stack", "baseline": 32, "textAlign": "center", "verticalAlign": "top" }, { "type": "rectangle", "version": 864, "versionNonce": 459154052, "isDeleted": false, "id": "snrOZbuQdOfsbwfDVsvOh", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 259.15066964285734, "y": -163.32142857142867, "strokeColor": "#607d8b", "backgroundColor": "transparent", "width": 711.9587053571425, "height": 743.0351562500001, "seed": 2001414322, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [] }, { "type": "arrow", "version": 746, "versionNonce": 926940476, "isDeleted": false, "id": "mnKhSKUhk7EFCekLtz1fQ", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 795.6113691236103, "y": 251.49637276785717, "strokeColor": "#ff4081", "backgroundColor": "transparent", "width": 0.5444303450863117, "height": 29.553292410714278, "seed": 1730055982, "groupIds": [], "strokeSharpness": "round", "boundElementIds": [], "startBinding": { "elementId": "pCkj2Qaeh9PDMAzSCWPM9", "focus": 0.032282012887604794, "gap": 1 }, "endBinding": { "elementId": "BK5gVfRenINsXjknIgSxf", "focus": -0.040579986296433085, "gap": 1.3568638392857224 }, "points": [ [ 0, 0 ], [ 0.5444303450863117, -29.553292410714278 ] ], "lastCommittedPoint": null }, { "type": "arrow", "version": 55, "versionNonce": 1268233916, "isDeleted": false, "id": "euFzlspcyBdei7ZjDGsTE", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 583.1283482142857, "y": 234.59709821428572, "strokeColor": "#607d8b", "backgroundColor": "transparent", "width": 62.05624021153369, "height": 0.7102786712734144, "seed": 1376797106, "groupIds": [], "strokeSharpness": "round", "boundElementIds": [], "startBinding": { "elementId": "zk1Tzfs7TN7NuvV9nUrkK", "focus": 1.4825585590033774, "gap": 16.743582589285722 }, "endBinding": { "elementId": "MSfRFHL5NGAG-wZc7hZub", "focus": 0.0075499005148347426, "gap": 19.697108002752145 }, "points": [ [ 0, 0 ], [ 62.05624021153369, -0.7102786712734144 ] ], "lastCommittedPoint": null } ], "appState": { "viewBackgroundColor": "#ffffff", "gridSize": null } } ================================================ FILE: assets/typical-stack-vs-layr-stack.excalidraw ================================================ { "type": "excalidraw", "version": 2, "source": "https://excalidraw.com", "elements": [ { "type": "rectangle", "version": 518, "versionNonce": 1142497202, "isDeleted": false, "id": "78pcPSx_TrkBrqMvVMUaS", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 314.42578125, "y": -47.8984375, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 250.83482142857142, "height": 67.28515625, "seed": 132882798, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [] }, { "type": "text", "version": 203, "versionNonce": 2102357102, "isDeleted": false, "id": "Tg35xQQF7BB9-_T5IlAPi", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 345.87890625, "y": -31.755859375, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 187, "height": 35, "seed": 74600110, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Data Access", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "rectangle", "version": 722, "versionNonce": 1392400754, "isDeleted": false, "id": "isBhZDRkcIoWGbySBsLEy", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 312.97265625, "y": 51.353515625, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 252.4587053571429, "height": 67.28515625, "seed": 298779118, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [] }, { "type": "text", "version": 403, "versionNonce": 633581230, "isDeleted": false, "id": "j7kpflnVnuaDP-1F5MCcc", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 340.42578125, "y": 67.49609375, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 199, "height": 35, "seed": 11834414, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Backend Model", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "rectangle", "version": 812, "versionNonce": 1284922162, "isDeleted": false, "id": "zk1Tzfs7TN7NuvV9nUrkK", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 312.97265625, "y": 150.568359375, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 253.4743303571429, "height": 67.28515625, "seed": 1135815662, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [ "_q_pfShbWagNPeqr2Cw4q", "CzbKjuh5LlBF8vEsdGoYs", "euFzlspcyBdei7ZjDGsTE" ] }, { "type": "text", "version": 429, "versionNonce": 854960366, "isDeleted": false, "id": "pbft6g1zEaKzjxE9qI4WH", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 366.1735491071429, "y": 166.48214285714283, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 152, "height": 35, "seed": 1694556846, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "API Server", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "rectangle", "version": 837, "versionNonce": 1598563570, "isDeleted": false, "id": "G_sH-_BODgjExxHFIJUO7", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 313.0546875, "y": 251.623046875, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 251.515625, "height": 67.28515625, "seed": 2087476462, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [ "P-5Se_P31c9YU_W_jiyRL", "CzbKjuh5LlBF8vEsdGoYs" ] }, { "type": "text", "version": 467, "versionNonce": 879647534, "isDeleted": false, "id": "FWR6GQyMu_VgSTbgtdktG", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 369.21205357142856, "y": 268.20647321428567, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 144, "height": 35, "seed": 82743086, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "API Client", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "rectangle", "version": 813, "versionNonce": 1419425458, "isDeleted": false, "id": "RxupO9IzGSd0WgL0ECubS", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 312.97265625, "y": 355.587890625, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 253.07812500000003, "height": 67.28515625, "seed": 290532466, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [ "GNYre13yKH_YUl3IHWLJV", "f72O2kwJb9RVWZ6WUwfn-", "tcnX3h7U3Y7nCrYotIHRV", "P-5Se_P31c9YU_W_jiyRL" ] }, { "type": "text", "version": 516, "versionNonce": 1620493678, "isDeleted": false, "id": "UJ8QgwFB2cterDA5_P1XR", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 337.83761160714283, "y": 370.9436383928572, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 205, "height": 35, "seed": 2132201010, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [ "uGtob-oyCnRKt3cM6Pyg_" ], "fontSize": 28, "fontFamily": 1, "text": "Frontend Model", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "rectangle", "version": 900, "versionNonce": 758452338, "isDeleted": false, "id": "I5x5Y1aUFonRrUCGi6_jw", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 312.97265625, "y": 458.373046875, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 252.57421875000006, "height": 67.28515625, "seed": 1935830894, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [ "GNYre13yKH_YUl3IHWLJV", "f72O2kwJb9RVWZ6WUwfn-", "qDACPeHQipsh0zAP-atrl" ] }, { "type": "text", "version": 591, "versionNonce": 951576494, "isDeleted": false, "id": "Ozy5i1Z2EEzX3kAVJ5IY9", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 335.1579241071429, "y": 474.50446428571445, "strokeColor": "#03a9f4", "backgroundColor": "transparent", "width": 207, "height": 35, "seed": 1037108850, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [ "uGtob-oyCnRKt3cM6Pyg_" ], "fontSize": 28, "fontFamily": 1, "text": "User Interface", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "rectangle", "version": 1077, "versionNonce": 1021194802, "isDeleted": false, "id": "BK5gVfRenINsXjknIgSxf", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "dotted", "roughness": 1, "opacity": 100, "angle": 0, "x": 695.0412946428572, "y": 153.30106026785717, "strokeColor": "#aeea00", "backgroundColor": "transparent", "width": 195.533482142857, "height": 67.28515625, "seed": 544079922, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [ "xQNTkIrt5bvAXhVmKvBxM", "hTkLQGX8fKKkms2V_vm0r", "CzbKjuh5LlBF8vEsdGoYs", "mnKhSKUhk7EFCekLtz1fQ" ] }, { "type": "text", "version": 786, "versionNonce": 754631150, "isDeleted": false, "id": "iQoD1G_MsPZm4xfuufUxe", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 734.5055803571429, "y": 168.5563616071429, "strokeColor": "#aeea00", "backgroundColor": "transparent", "width": 113, "height": 35, "seed": 48441842, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Backend", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "rectangle", "version": 1219, "versionNonce": 1182604274, "isDeleted": false, "id": "pCkj2Qaeh9PDMAzSCWPM9", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "dotted", "roughness": 1, "opacity": 100, "angle": 0, "x": 694.9520089285714, "y": 252.49637276785717, "strokeColor": "#aeea00", "backgroundColor": "transparent", "width": 193.7477678571429, "height": 67.28515625, "seed": 374338158, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [ "xQNTkIrt5bvAXhVmKvBxM", "hTkLQGX8fKKkms2V_vm0r", "mnKhSKUhk7EFCekLtz1fQ" ] }, { "type": "text", "version": 809, "versionNonce": 1080579118, "isDeleted": false, "id": "-CvZWfngqmHMBLZxpCqri", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 732.0145089285714, "y": 269.67131696428584, "strokeColor": "#aeea00", "backgroundColor": "transparent", "width": 119, "height": 35, "seed": 415462574, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 28, "fontFamily": 1, "text": "Frontend", "baseline": 25, "textAlign": "center", "verticalAlign": "top" }, { "type": "rectangle", "version": 1500, "versionNonce": 404278706, "isDeleted": false, "id": "MSfRFHL5NGAG-wZc7hZub", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 664.8816964285714, "y": 117.36356026785717, "strokeColor": "#aeea00", "backgroundColor": "transparent", "width": 252.3643973214285, "height": 231.47656249999994, "seed": 153297006, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [ "CzbKjuh5LlBF8vEsdGoYs", "euFzlspcyBdei7ZjDGsTE" ] }, { "type": "text", "version": 346, "versionNonce": 1998441774, "isDeleted": false, "id": "HblX02yOmNDLcQXmIx14_", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 318.7762276785714, "y": -124.638671875, "strokeColor": "#eceff1", "backgroundColor": "transparent", "width": 244, "height": 45, "seed": 1398208946, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 36, "fontFamily": 1, "text": "Typical Stack", "baseline": 32, "textAlign": "center", "verticalAlign": "top" }, { "type": "text", "version": 665, "versionNonce": 848204036, "isDeleted": false, "id": "oi9Jo0SrqAcTWeuWmDnFT", "fillStyle": "hachure", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 670.4157366071428, "y": 42.34793526785717, "strokeColor": "#eceff1", "backgroundColor": "transparent", "width": 241, "height": 45, "seed": 1175668722, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "fontSize": 36, "fontFamily": 1, "text": "Layr Stack", "baseline": 32, "textAlign": "center", "verticalAlign": "top" }, { "type": "rectangle", "version": 863, "versionNonce": 1082652526, "isDeleted": false, "id": "snrOZbuQdOfsbwfDVsvOh", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 259.15066964285734, "y": -163.32142857142867, "strokeColor": "#eceff1", "backgroundColor": "transparent", "width": 711.9587053571425, "height": 743.0351562500001, "seed": 2001414322, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [] }, { "type": "arrow", "version": 745, "versionNonce": 1705508466, "isDeleted": false, "id": "mnKhSKUhk7EFCekLtz1fQ", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 795.6113691236103, "y": 251.49637276785717, "strokeColor": "#aeea00", "backgroundColor": "transparent", "width": 0.5444303450863117, "height": 29.553292410714278, "seed": 1730055982, "groupIds": [], "strokeSharpness": "round", "boundElementIds": [], "startBinding": { "elementId": "pCkj2Qaeh9PDMAzSCWPM9", "focus": 0.032282012887604794, "gap": 1 }, "endBinding": { "elementId": "BK5gVfRenINsXjknIgSxf", "focus": -0.040579986296433085, "gap": 1.3568638392857224 }, "points": [ [ 0, 0 ], [ 0.5444303450863117, -29.553292410714278 ] ], "lastCommittedPoint": null }, { "type": "arrow", "version": 54, "versionNonce": 79913010, "isDeleted": false, "id": "euFzlspcyBdei7ZjDGsTE", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 583.1283482142857, "y": 234.59709821428572, "strokeColor": "#eceff1", "backgroundColor": "transparent", "width": 62.05624021153369, "height": 0.7102786712734144, "seed": 1376797106, "groupIds": [], "strokeSharpness": "round", "boundElementIds": [], "startBinding": { "elementId": "zk1Tzfs7TN7NuvV9nUrkK", "focus": 1.4825585590033774, "gap": 16.743582589285722 }, "endBinding": { "elementId": "MSfRFHL5NGAG-wZc7hZub", "focus": 0.0075499005148347426, "gap": 19.697108002752145 }, "points": [ [ 0, 0 ], [ 62.05624021153369, -0.7102786712734144 ] ], "lastCommittedPoint": null } ], "appState": { "viewBackgroundColor": "#000", "gridSize": null } } ================================================ FILE: docs/build.js ================================================ const {buildDocumentation, freezeDocumentation} = require('@mvila/simple-doc'); const {copySync, removeSync, readJsonSync, writeJSONSync} = require('fs-extra'); const path = require('path'); const VERSION = 'v2'; const SOURCE_DIRECTORY = './contents'; const BUILD_DIRECTORY = './build'; const WEBSITE_DIRECTORY = '../website/frontend/public/docs'; const WEBSITE_INDEX_FILE = '../website/frontend/src/docs.json'; const sourceDirectory = path.resolve(__dirname, SOURCE_DIRECTORY); const buildDirectory = path.resolve(__dirname, BUILD_DIRECTORY); const websiteDirectory = path.resolve(__dirname, WEBSITE_DIRECTORY); const websiteIndexFile = path.resolve(__dirname, WEBSITE_INDEX_FILE); buildDocumentation(sourceDirectory, buildDirectory); freezeDocumentation(buildDirectory); console.log(`Writing index file to '${path.relative(process.cwd(), websiteIndexFile)}'...`); const builtIndexFile = path.join(buildDirectory, 'index.json'); const builtIndex = readJsonSync(builtIndexFile); const websiteIndex = readJsonSync(websiteIndexFile); websiteIndex.versions[VERSION] = builtIndex; writeJSONSync(websiteIndexFile, websiteIndex, {spaces: 2}); const websiteDirectoryWithVersion = path.join(websiteDirectory, VERSION); console.log( `Copying contents to '${path.relative(process.cwd(), websiteDirectoryWithVersion)}'...` ); removeSync(websiteDirectoryWithVersion); copySync(buildDirectory, websiteDirectoryWithVersion, { filter(source) { return source !== builtIndexFile; } }); ================================================ FILE: docs/contents/concepts/coming-soon.md ================================================ ### Concepts #### Coming Soon This section will introduce the basic concepts of Layr, such as: - Components - Controlled attributes and methods - Cross-layer inheritance - Storage - Routes and wrappers Unfortunately, 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. More documentation will come for sure, but please be patient. If you cannot wait and feel adventurous, you can figure out how to build an app with Layr by checking out: - The ["Hello, World!"](https://layrjs.com/docs/v2/introduction/hello-world) introductory app. - The pretty exhaustive ["Reference"](https://layrjs.com/docs/v2/reference) section. - Some [examples](https://layrjs.com/docs/v2/introduction/introduction#examples) of simple full-stack apps built with Layr. ================================================ FILE: docs/contents/guides/coming-soon.md ================================================ ### Guides #### Coming Soon This section will contain some guides explaining how to achieve the most common tasks and features with Layr, such as: - Local development - Layouts and pages - Data fetching and storage - Authentication - Testing - Deployment Unfortunately, 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. More documentation will come for sure, but please be patient. If you cannot wait and feel adventurous, you can figure out how to build an app with Layr by checking out: - The ["Hello, World!"](https://layrjs.com/docs/v2/introduction/hello-world) introductory app. - The pretty exhaustive ["Reference"](https://layrjs.com/docs/v2/reference) section. - Some [examples](https://layrjs.com/docs/v2/introduction/introduction#examples) of simple full-stack apps built with Layr. ================================================ FILE: docs/contents/index.json ================================================ { "books": [ { "title": "Getting Started", "slug": "introduction", "chapters": [ { "title": "Introduction", "slug": "introduction", "file": "introduction/introduction.md" }, { "title": "Hello, World!", "slug": "hello-world", "file": "introduction/hello-world/hello-world.md", "assets": "introduction/hello-world/assets" }, { "title": "Storing Data", "slug": "storing-data", "file": "introduction/storing-data.md" }, { "title": "Handling Authorization", "slug": "handling-authorization", "file": "introduction/handling-authorization.md" } ] }, { "title": "Concepts", "slug": "concepts", "chapters": [ { "title": "Coming Soon", "slug": "coming-soon", "file": "concepts/coming-soon.md" } ] }, { "title": "Guides", "slug": "guides", "chapters": [ { "title": "Coming Soon", "slug": "coming-soon", "file": "guides/coming-soon.md" } ] }, { "title": "Reference", "slug": "reference", "chapters": [ { "title": "Component", "slug": "component", "file": "reference/component.md", "source": [ "../../packages/component/src/component.ts", "../../packages/component/src/cloning.ts", "../../packages/component/src/forking.ts", "../../packages/component/src/merging.ts", "../../packages/component/src/serialization.ts", "../../packages/component/src/deserialization.ts", "../../packages/component/src/decorators.ts", "../../packages/component/src/utilities.ts" ], "category": "Basics" }, { "title": "EmbeddedComponent", "slug": "embedded-component", "file": "reference/embedded-component.md", "source": "../../packages/component/src/embedded-component.ts", "category": "Basics" }, { "title": "Property", "slug": "property", "file": "reference/property.md", "source": "../../packages/component/src/properties/property.ts", "category": "Basics" }, { "title": "Attribute", "slug": "attribute", "file": "reference/attribute.md", "source": "../../packages/component/src/properties/attribute.ts", "category": "Basics" }, { "title": "IdentifierAttribute", "slug": "identifier-attribute", "file": "reference/identifier-attribute.md", "source": "../../packages/component/src/properties/identifier-attribute.ts", "category": "Basics" }, { "title": "PrimaryIdentifierAttribute", "slug": "primary-identifier-attribute", "file": "reference/primary-identifier-attribute.md", "source": "../../packages/component/src/properties/primary-identifier-attribute.ts", "category": "Basics" }, { "title": "SecondaryIdentifierAttribute", "slug": "secondary-identifier-attribute", "file": "reference/secondary-identifier-attribute.md", "source": "../../packages/component/src/properties/secondary-identifier-attribute.ts", "category": "Basics" }, { "title": "Method", "slug": "method", "file": "reference/method.md", "source": "../../packages/component/src/properties/method.ts", "category": "Basics" }, { "title": "ValueType", "slug": "value-type", "file": "reference/value-type.md", "source": "../../packages/component/src/properties/value-types/value-type.ts", "category": "Basics" }, { "title": "Sanitizer", "slug": "sanitizer", "file": "reference/sanitizer.md", "source": "../../packages/component/src/sanitization/sanitizer.ts", "category": "Basics" }, { "title": "Validator", "slug": "validator", "file": "reference/validator.md", "source": "../../packages/component/src/validation/validator.ts", "category": "Basics" }, { "title": "AttributeSelector", "slug": "attribute-selector", "file": "reference/attribute-selector.md", "source": "../../packages/component/src/properties/attribute-selector.ts", "category": "Basics" }, { "title": "IdentityMap", "slug": "identity-map", "file": "reference/identity-map.md", "source": "../../packages/component/src/identity-map.ts", "category": "Basics" }, { "title": "ComponentClient", "slug": "component-client", "file": "reference/component-client.md", "source": "../../packages/component-client/src/component-client.ts", "category": "Communication" }, { "title": "ComponentServer", "slug": "component-server", "file": "reference/component-server.md", "source": "../../packages/component-server/src/component-server.ts", "category": "Communication" }, { "title": "ComponentHTTPClient", "slug": "component-http-client", "file": "reference/component-http-client.md", "source": "../../packages/component-http-client/src/component-http-client.ts", "category": "Communication" }, { "title": "ComponentHTTPServer", "slug": "component-http-server", "file": "reference/component-http-server.md", "source": "../../packages/component-http-server/src/component-http-server.ts", "category": "Communication" }, { "title": "component-express-middleware", "slug": "component-express-middleware", "file": "reference/component-express-middleware.md", "source": "../../packages/component-express-middleware/src/component-express-middleware.ts", "category": "Communication" }, { "title": "component-koa-middleware", "slug": "component-koa-middleware", "file": "reference/component-koa-middleware.md", "source": "../../packages/component-koa-middleware/src/component-koa-middleware.ts", "category": "Communication" }, { "title": "Storable()", "slug": "storable", "file": "reference/storable.md", "source": [ "../../packages/storable/src/storable.ts", "../../packages/storable/src/decorators.ts", "../../packages/storable/src/utilities.ts" ], "category": "Storage" }, { "title": "StorableProperty", "slug": "storable-property", "file": "reference/storable-property.md", "source": ["../../packages/storable/src/properties/storable-property.ts"], "category": "Storage" }, { "title": "StorableAttribute", "slug": "storable-attribute", "file": "reference/storable-attribute.md", "source": ["../../packages/storable/src/properties/storable-attribute.ts"], "category": "Storage" }, { "title": "StorablePrimaryIdentifierAttribute", "slug": "storable-primary-identifier-attribute", "file": "reference/storable-primary-identifier-attribute.md", "source": [ "../../packages/storable/src/properties/storable-primary-identifier-attribute.ts" ], "category": "Storage" }, { "title": "StorableSecondaryIdentifierAttribute", "slug": "storable-secondary-identifier-attribute", "file": "reference/storable-secondary-identifier-attribute.md", "source": [ "../../packages/storable/src/properties/storable-secondary-identifier-attribute.ts" ], "category": "Storage" }, { "title": "StorableMethod", "slug": "storable-method", "file": "reference/storable-method.md", "source": ["../../packages/storable/src/properties/storable-method.ts"], "category": "Storage" }, { "title": "Query", "slug": "query", "file": "reference/query.md", "source": "../../packages/storable/src/query.ts", "category": "Storage" }, { "title": "Index", "slug": "index", "file": "reference/index.md", "source": "../../packages/storable/src/index-class.ts", "category": "Storage" }, { "title": "Store", "slug": "store", "file": "reference/store.md", "source": "../../packages/store/src/store.ts", "category": "Storage" }, { "title": "MongoDBStore", "slug": "mongodb-store", "file": "reference/mongodb-store.md", "source": "../../packages/mongodb-store/src/mongodb-store.ts", "category": "Storage" }, { "title": "MemoryStore", "slug": "memory-store", "file": "reference/memory-store.md", "source": "../../packages/memory-store/src/memory-store.ts", "category": "Storage" }, { "title": "Routable()", "slug": "routable", "file": "reference/routable.md", "source": [ "../../packages/routable/src/routable.ts", "../../packages/routable/src/decorators.ts", "../../packages/routable/src/utilities.ts" ], "category": "Routing" }, { "title": "Addressable", "slug": "addressable", "file": "reference/addressable.md", "source": "../../packages/routable/src/addressable.ts", "category": "Routing" }, { "title": "Route", "slug": "route", "file": "reference/route.md", "source": "../../packages/routable/src/route.ts", "category": "Routing" }, { "title": "Wrapper", "slug": "wrapper", "file": "reference/wrapper.md", "source": "../../packages/routable/src/wrapper.ts", "category": "Routing" }, { "title": "Navigator", "slug": "navigator", "file": "reference/navigator.md", "source": [ "../../packages/navigator/src/navigator.ts", "../../packages/navigator/src/utilities.ts" ], "category": "Routing" }, { "title": "BrowserNavigator", "slug": "browser-navigator", "file": "reference/browser-navigator.md", "source": "../../packages/browser-navigator/src/browser-navigator.ts", "category": "Routing" }, { "title": "MemoryNavigator", "slug": "memory-navigator", "file": "reference/memory-navigator.md", "source": "../../packages/memory-navigator/src/memory-navigator.ts", "category": "Routing" }, { "title": "WithRoles()", "slug": "with-roles", "file": "reference/with-roles.md", "source": [ "../../packages/with-roles/src/with-roles.ts", "../../packages/with-roles/src/decorators.ts", "../../packages/with-roles/src/utilities.ts" ], "category": "Authorization" }, { "title": "Role", "slug": "role", "file": "reference/role.md", "source": "../../packages/with-roles/src/role.ts", "category": "Authorization" }, { "title": "aws-integration", "slug": "aws-integration", "file": "reference/aws-integration.md", "source": [ "../../packages/aws-integration/src/index.ts", "../../packages/aws-integration/src/lambda-handler.ts" ], "category": "Integrations" }, { "title": "react-integration", "slug": "react-integration", "file": "reference/react-integration.md", "source": [ "../../packages/react-integration/src/index.ts", "../../packages/react-integration/src/components.tsx", "../../packages/react-integration/src/decorators.tsx", "../../packages/react-integration/src/hooks.ts" ], "category": "Integrations" }, { "title": "Observable()", "slug": "observable", "file": "reference/observable.md", "source": "../../packages/observable/src/observable.ts", "category": "Utilities" } ] } ] } ================================================ FILE: docs/contents/introduction/handling-authorization.md ================================================ ### Handling Authorization #### Coming Soon This tutorial will expand the ["Hello, World!"](https://layrjs.com/docs/v2/introduction/hello-world) app to show you how Layr handles user authorization. Unfortunately, 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. This tutorial will come for sure, but please be patient. If you cannot wait and feel adventurous, you can figure out how to handle authorization with Layr by checking out: - The [`WithRoles()`](https://layrjs.com/docs/v2/reference/with-roles) mixin in the "Reference" section. - Some examples of simple full-stack apps handling authorization with Layr: - [Layr Website (TS)](https://github.com/layrjs/layr/tree/master/website) - [CodebaseShow (TS)](https://github.com/codebaseshow/codebaseshow) - [RealWorld Example App (JS)](https://github.com/layrjs/react-layr-realworld-example-app) ================================================ FILE: docs/contents/introduction/hello-world/hello-world.md ================================================ ### Hello, World! Let'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! > **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. > **TLDR**: The completed app is available in the [Layr repository](https://github.com/layrjs/layr/tree/master/examples/v2/hello-world-js)[Layr repository](https://github.com/layrjs/layr/tree/master/examples/v2/hello-world-ts). #### Prerequisites - 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. - You have [Node.js](https://nodejs.org/) v16 or newer installed. - You are familiar with [React](https://reactjs.org/), which is used in this tutorial. #### Creating the App To make things easier, we'll use [Boostr](https://boostr.dev) to create the app and manage the local development environment. Boostr 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. So, first of all, we have to install Boostr. Run the following command in your terminal: ```sh npm install --global boostr ``` > **Notes**: > > - Depending on your Node.js installation, you may have to prefix the command with `sudo` so the package can be installed globally. > - 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. Then, run the following commands to create a directory for the app and initialize it: ```sh mkdir hello-world cd hello-world boostr initialize @boostr/web-app-js ``` ```sh mkdir hello-world cd hello-world boostr initialize @boostr/web-app-ts ``` Finally, you can open the `hello-world` directory in your favorite [IDE](https://en.wikipedia.org/wiki/Integrated_development_environment) to explore the initial codebase. > **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. We 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. So, we will only focus on two files: - `frontend/src/components/application.jsx`: The root [component](https://layrjs.com/docs/v2/reference/component) of the app frontend. - `backend/src/components/application.js`: The root [component](https://layrjs.com/docs/v2/reference/component) of the app backend. - `frontend/src/components/application.tsx`: The root [component](https://layrjs.com/docs/v2/reference/component) of the app frontend. - `backend/src/components/application.ts`: The root [component](https://layrjs.com/docs/v2/reference/component) of the app backend. #### Starting the App Start the app in development mode with the following command: ```sh boostr start ``` The terminal should output something like this: ```txt [database] MongoDB server started at mongodb://localhost:18160/ [backend] Build succeeded (bundle size: 2.06MB) [backend] Component HTTP server started at http://localhost:18159/ [frontend] Build succeeded (bundle size: 1.34MB) [frontend] Single-page application server started at http://localhost:18158/ ``` > **Notes**: > > - 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. > - 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. The last line in the terminal output should include an URL you can open in a browser to display the app. At this point, you should see something like this in your browser:

Screenshot of the app initialized by Boostr

#### Taking a Look at the Initial App Let's see what we have for now. When 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. ##### Frontend Here's what the initial frontend root [component](https://layrjs.com/docs/v2/reference/component) (`Application`) looks like: ```js // JS // frontend/src/components/application.jsx import {Routable} from '@layr/routable'; import React from 'react'; import {layout, page, useData} from '@layr/react-integration'; export const extendApplication = (Base) => { class Application extends Routable(Base) { @layout('/') static MainLayout({children}) { return ( <>

{process.env.APPLICATION_NAME}

{children()} ); } @page('[/]') static MainPage() { return useData( async () => await this.isHealthy(), (isHealthy) =>

The app is {isHealthy ? 'healthy' : 'unhealthy'}.

); } @page('[/]*') static NotFoundPage() { return ( <>

Page not found

Sorry, there is nothing here.

); } } return Application; }; ``` ```ts // TS // frontend/src/components/application.tsx import {Routable} from '@layr/routable'; import React, {Fragment} from 'react'; import {layout, page, useData} from '@layr/react-integration'; import type {Application as BackendApplication} from '../../../backend/src/components/application'; export const extendApplication = (Base: typeof BackendApplication) => { class Application extends Routable(Base) { declare ['constructor']: typeof Application; @layout('/') static MainLayout({children}: {children: () => any}) { return ( <>

{process.env.APPLICATION_NAME}

{children()} ); } @page('[/]') static MainPage() { return useData( async () => await this.isHealthy(), (isHealthy) =>

The app is {isHealthy ? 'healthy' : 'unhealthy'}.

); } @page('[/]*') static NotFoundPage() { return ( <>

Page not found

Sorry, there is nothing here.

); } } return Application; }; export declare const Application: ReturnType; export type Application = InstanceType; ``` If 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. `MainLayout()` implements a layout for all the pages of the app: - 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. - It renders the name of the app (using the `APPLICATION_NAME` [environment variable](https://github.com/boostrjs/boostr#environment-variables)) in an `

` HTML tag, which is nested into a link pointing to the app's main page (using the [``](https://layrjs.com/docs/v2/reference/routable#route-decorator) React element). - It calls the `children` prop to render the content of the pages using this layout. `MainPage()` implements the main page of the app: - 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. - 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 `

The app is {isHealthy ? 'healthy' : 'unhealthy'}.

`). `NotFoundPage()` implements a page that is displayed when a user goes to an URL that is not handled by the app: - 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. - It renders some simple HTML tags and texts indicating that the page cannot be found. ##### Backend Here's what the initial backend root [component](https://layrjs.com/docs/v2/reference/component) (`Application`) looks like: ```js // JS // backend/src/components/application.js import {Component, method, expose} from '@layr/component'; export class Application extends Component { @expose({call: true}) @method() static async isHealthy() { return true; } } ``` ```ts // TS // backend/src/components/application.ts import {Component, method, expose} from '@layr/component'; export class Application extends Component { @expose({call: true}) @method() static async isHealthy() { return true; } } ``` This component is straightforward. `isHealthy()` implements a class method that the frontend can call to check whether the app is healthy: - It is decorated with [`@expose({call: true})`](https://layrjs.com/docs/v2/reference/component#expose-decorator), which exposes the method to the frontend. - 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. - It always returns `true`, which is rather pointless. An actual `isHealthy()` method would, for example, check whether a database responds correctly. #### Displaying "Hello, World!" in the Frontend It's about time to write some code. We'll start gently by modifying the frontend to display "Hello, World!" instead of "The app is healthy.". Modify the `MainPage()` class method in the `frontend/src/components/application.jsx``frontend/src/components/application.tsx` file as follows: ```ts @page('[/]') static MainPage() { return

Hello, World!

; } ``` Save the file, and your browser should automatically refresh the page with the following contents:

Screenshot of the app displaying 'Hello, World!'

#### Adding a Page in the Frontend So the frontend is now displaying "Hello, World!" and we could call it a day. But that was a bit too easy, don't you think? Let's spice this tutorial a little by adding a page in charge of displaying the "Hello, World!" message. Add the following class method in the `frontend/src/components/application.jsx``frontend/src/components/application.tsx` file: ```ts @page('[/]hello-world') static HelloWorldPage() { return

Hello, World!

; } ``` That'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. Let's do so by modifying the `MainPage()` class method as follows: ```ts @page('[/]') static MainPage() { return (

See the "Hello, World!" page

); } ``` Hold 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. We 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 `` tag referencing the URL path of the page. Your browser should now display the following page:

Screenshot of the app displaying a link to the 'Hello, World!' page

#### Getting the "Hello, World" Message from the Backend At 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. Let's fix that by moving the "business logic" generating the "Hello, World!" message to the backend. First, add the following class method in the `backend/src/components/application.js``backend/src/components/application.ts` file: ```ts @expose({call: true}) @method() static async getHelloWorld() { return 'Hello, World!'; } ``` This 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. > **Note**: Since the `isHealthy()` class method is not used anymore, you can remove it if you want. Save the file to restart the backend, and then modify the `HelloWorldPage()` class method in the `frontend/src/components/application.jsx``frontend/src/components/application.tsx` file as follows: ```ts @page('[/]hello-world') static HelloWorldPage() { return useData( async () => await this.getHelloWorld(), (helloWorld) =>

{helloWorld}

); } ``` As 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 `

{helloWorld}

`). If 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. #### One Last Thing Technically, we now have a full-stack app involving a frontend and a backend. But the app is a bit static. The backend always returns the same `'Hello, World'` string, which is rather boring. Let's make the app more dynamic by modifying the `getHelloWorld()` class method in the `backend/src/components/application.js``backend/src/components/application.ts` file as follows: ```ts @expose({call: true}) @method() static async getHelloWorld() { const translations = ['Hello, World!', 'Bonjour le monde !', 'こんにちは世界!']; const translation = translations[Math.round(Math.random() * (translations.length - 1))]; return translation; } ``` Save 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. Here's an example showing the Japanese translation:

Screenshot of the app displaying 'Hello, World!' in Japanese

We 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. #### Wrapping Up In this first tutorial, we saw how to build a basic full-stack app with Layr: - 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. - 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). - 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. ================================================ FILE: docs/contents/introduction/introduction.md ================================================ ### Introduction > **Note**: Layr v2 is published on NPM, but the documentation is still a work in progress. #### Overview Layr is a set of JavaScript/TypeScript libraries that dramatically simplify the development of highly dynamic full-stack apps. Typically, 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.) Separating 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. Layr 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. On 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. On 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. #### Made for Highly Dynamic Apps Layr stands out for building apps that offer rich user interfaces, such as the good old desktop apps. Even if they both run in a browser, we should clearly differentiate "websites" and "web apps". ##### Websites Websites provide fast load time, good [SEO](https://en.wikipedia.org/wiki/Search_engine_optimization), and a few dynamic parts. Some 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/)). Layr 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. So, 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/). ##### Web Apps Web apps provide rich user interfaces and are all made of dynamic parts. Some 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/)). Layr is made for building these kinds of apps with a straightforward architecture: - The frontend exposes an interface for _humans_. - The backend exposes an interface for _computers_. Note 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/). #### Core Features Layr provides everything you need to build an app from start to finish: - **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. - **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. - **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. - **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. - **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). - **Authorization**: [User-role-based](https://layrjs.com/docs/v2/reference/with-roles) authorizations can be set to restrict some attributes or methods. - **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. - **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. #### Core Principles Here's a quick taste of the core principles upon which Layr is built: - **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. - **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. - **100% DRY**: A Layr app can be fully [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) (i.e., zero knowledge duplication). - **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. - **Unopinionated**: Layr has strong opinions about itself but doesn't force you to use any external library, service, or tool. #### Command-Line Interface Layr is just a set of low-level JavaScript/TypeScript libraries and does not come with a CLI. So, you can use Layr with any development and deployment tools. However, we know that setting up a development environment and a deployment mechanism can be challenging. So, we created [Boostr](https://boostr.dev), a companion tool that takes care of everything you need to build and deploy a Layr app. Check out the [Boostr documentation](https://boostr.dev/docs) to find out more. #### Compatibility Layr is implemented in [TypeScript](https://www.typescriptlang.org/), but you can use either JavaScript or TypeScript to build your app. If 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". If you are using TypeScript, all you need is the TypeScript compiler ([`tsc`](https://www.typescriptlang.org/docs/handbook/compiler-options.html)). > **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. To run your app, you'll need a JavaScript runtime for both the frontend and the backend. ##### Frontend ###### Web Any modern browser should work fine. Here are the minimum versions with which Layr is tested: - Chrome v55 - Safari v11 - Firefox v54 - Edge Chromium ###### Mobile and Desktop Any 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. ##### Backend Any environment running [Node.js](https://nodejs.org/) v16 or later is supported. #### Examples Here are some examples of simple full-stack apps that you can check out: - [CRUD Example App (JS)](https://github.com/layrjs/crud-example-app-js-boostr) - [CRUD Example App (TS)](https://github.com/layrjs/crud-example-app-ts-boostr) - [Layr Website (TS)](https://github.com/layrjs/layr/tree/master/website) - [CodebaseShow (TS)](https://github.com/codebaseshow/codebaseshow) - [RealWorld Example App (JS)](https://github.com/layrjs/react-layr-realworld-example-app) ================================================ FILE: docs/contents/introduction/storing-data.md ================================================ ### Storing Data #### Coming Soon This tutorial will expand the ["Hello, World!"](https://layrjs.com/docs/v2/introduction/hello-world) app to show you how Layr handles database storage. Unfortunately, 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. This tutorial will come for sure, but please be patient. If you cannot wait and feel adventurous, you can figure out how to store data with Layr by checking out: - The [`Storable()`](https://layrjs.com/docs/v2/reference/storable) mixin in the "Reference" section. - Some [examples](https://layrjs.com/docs/v2/introduction/introduction#examples) of simple full-stack apps built with Layr. ================================================ FILE: docs/package.json ================================================ { "name": "@layr/docs", "version": "1.0.0", "private": true, "description": "Layr documentation", "keywords": [], "author": "Manuel Vila ", "license": "MIT", "repository": "https://github.com/layrjs/layr/tree/master/docs", "scripts": { "build": "node ./build.js" }, "dependencies": { "@mvila/simple-doc": "^0.1.138", "fs-extra": "^11.1.0" } } ================================================ FILE: examples/v1/counter/README.md ================================================ # Counter A simple example to introduce the core concepts of Layr. ## Usage Install the npm dependencies with: ```sh npm install ``` Run the example with: ```sh npx babel-node ./src/frontend.js ``` ================================================ FILE: examples/v1/counter/babel.config.json ================================================ { "presets": [["@babel/preset-env", {"targets": {"node": "10"}}]], "plugins": [ ["@babel/plugin-proposal-decorators", {"legacy": true}], ["@babel/plugin-proposal-class-properties"] ] } ================================================ FILE: examples/v1/counter/jsconfig.json ================================================ { "compilerOptions": { "target": "ES2017", "module": "CommonJS", "checkJs": false, "experimentalDecorators": true } } ================================================ FILE: examples/v1/counter/package.json ================================================ { "name": "counter", "version": "1.0.0", "description": "A simple example to introduce the core concepts of Layr", "private": true, "dependencies": { "@layr/component": "^1.0.0", "@layr/component-client": "^1.0.0", "@layr/component-server": "^1.0.0" }, "devDependencies": { "@babel/core": "^7.11.1", "@babel/node": "^7.10.5", "@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/plugin-proposal-decorators": "^7.10.5", "@babel/preset-env": "^7.11.0" } } ================================================ FILE: examples/v1/counter/src/backend.js ================================================ import {Component, primaryIdentifier, attribute, method, expose} from '@layr/component'; import {ComponentServer} from '@layr/component-server'; class Counter extends Component { // We need a primary identifier so a Counter instance // can be transported between the frontend and the backend // while keeping it's identity @expose({get: true, set: true}) @primaryIdentifier() id; // The counter's value is exposed to the frontend @expose({get: true, set: true}) @attribute('number') value = 0; // And the "business logic" is exposed as well @expose({call: true}) @method() increment() { this.value++; } } // We serve the Counter through a ComponentServer export const server = new ComponentServer(Counter); ================================================ FILE: examples/v1/counter/src/frontend.js ================================================ import {ComponentClient} from '@layr/component-client'; import {server} from './backend'; // We create a client that is connected to the backend's server const client = new ComponentClient(server); // We get the backend's Counter component const BackendCounter = client.getComponent(); // We extends the backend's Counter component so we can override the increment() method class Counter extends BackendCounter { increment() { super.increment(); // The backend's `increment()` method is invoked console.log(this.value); // Some additional code is executed in the frontend } } // Lastly, we consume the Counter const counter = new Counter(); counter.increment(); ================================================ FILE: examples/v1/counter-with-create-react-app-ts/README.md ================================================ # Counter with CreateReactApp (TS) A simple example to introduce the core concepts of Layr. ## Install Install the npm dependencies with: ```sh npm install ``` ## Usage ### Running the app in development mode Execute the following command: ```sh npm run start ``` The app should then be available at http://localhost:3000. ### Debugging #### Client Add the following entry in the local storage of your browser: ``` | Key | Value | | ----- | --------- | | debug | layr:* | ``` #### Server Add the following environment variables when starting the app: ```sh DEBUG=layr:* DEBUG_DEPTH=10 ``` ================================================ FILE: examples/v1/counter-with-create-react-app-ts/backend/package.json ================================================ { "name": "counter-with-create-react-app-ts-backend", "version": "1.0.0", "private": true, "author": "Manuel Vila ", "license": "MIT", "scripts": { "start": "nodemon --watch ./src --exec ts-node ./src/http-server.ts" }, "dependencies": { "@layr/component": "^1.0.0", "@layr/component-server": "^1.0.0", "tslib": "^2.0.3" }, "devDependencies": { "@layr/component-http-server": "^1.0.0", "@types/node": "^14.11.8", "nodemon": "^2.0.5", "ts-node": "^9.0.0", "typescript": "^4.0.3" } } ================================================ FILE: examples/v1/counter-with-create-react-app-ts/backend/src/components/counter.ts ================================================ import {Component, primaryIdentifier, attribute, method, expose} from '@layr/component'; export class Counter extends Component { @expose({get: true, set: true}) @primaryIdentifier() id!: string; @expose({get: true, set: true}) @attribute('number') value = 0; @expose({call: true}) @method() async increment() { this.value++; } } ================================================ FILE: examples/v1/counter-with-create-react-app-ts/backend/src/http-server.ts ================================================ import {ComponentHTTPServer} from '@layr/component-http-server'; import {server} from './server'; const httpServer = new ComponentHTTPServer(server, {port: 3001}); httpServer.start(); ================================================ FILE: examples/v1/counter-with-create-react-app-ts/backend/src/server.ts ================================================ import {ComponentServer} from '@layr/component-server'; import {Counter} from './components/counter'; export const server = new ComponentServer(Counter); ================================================ FILE: examples/v1/counter-with-create-react-app-ts/backend/tsconfig.json ================================================ { "include": ["src/**/*"], "compilerOptions": { "target": "ES2017", "module": "CommonJS", "lib": ["ESNext", "DOM"], "jsx": "react", "sourceMap": true, "importHelpers": true, "strict": true, "moduleResolution": "node", "esModuleInterop": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "experimentalDecorators": true, "resolveJsonModule": true, "stripInternal": true, "skipLibCheck": true } } ================================================ FILE: examples/v1/counter-with-create-react-app-ts/frontend/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # production /build # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: examples/v1/counter-with-create-react-app-ts/frontend/package.json ================================================ { "name": "counter-with-create-react-app-ts-frontend", "version": "1.0.0", "private": true, "author": "Manuel Vila ", "license": "MIT", "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "dependencies": { "@layr/component": "^1.0.6", "@layr/component-http-client": "^1.0.3", "@layr/react-integration": "^1.0.3", "@types/node": "^14.11.8", "@types/react": "^16.9.52", "@types/react-dom": "^16.9.8", "react": "^16.13.1", "react-dom": "^16.13.1", "react-scripts": "3.4.3", "typescript": "^4.0.3" }, "eslintConfig": { "extends": "react-app" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } } ================================================ FILE: examples/v1/counter-with-create-react-app-ts/frontend/public/index.html ================================================ React App
================================================ FILE: examples/v1/counter-with-create-react-app-ts/frontend/public/manifest.json ================================================ { "short_name": "React App", "name": "Create React App Sample", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" }, { "src": "logo192.png", "type": "image/png", "sizes": "192x192" }, { "src": "logo512.png", "type": "image/png", "sizes": "512x512" } ], "start_url": ".", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" } ================================================ FILE: examples/v1/counter-with-create-react-app-ts/frontend/public/robots.txt ================================================ # https://www.robotstxt.org/robotstxt.html User-agent: * Disallow: ================================================ FILE: examples/v1/counter-with-create-react-app-ts/frontend/src/components/counter.tsx ================================================ import {ComponentHTTPClient} from '@layr/component-http-client'; import React from 'react'; import {view} from '@layr/react-integration'; import type {Counter as BackendCounter} from '../../../backend/src/components/counter'; export const getCounter = async () => { const client = new ComponentHTTPClient('http://localhost:3001'); const BackendCounterProxy = (await client.getComponent()) as typeof BackendCounter; class Counter extends BackendCounterProxy { @view() Main() { return (
); } } return Counter; }; ================================================ FILE: examples/v1/counter-with-create-react-app-ts/frontend/src/index.tsx ================================================ import React from 'react'; import ReactDOM from 'react-dom'; import {getCounter} from './components/counter'; (async () => { const Counter = await getCounter(); const counter = new Counter(); ReactDOM.render( , document.getElementById('root') ); })(); ================================================ FILE: examples/v1/counter-with-create-react-app-ts/frontend/src/react-app-env.d.ts ================================================ /// ================================================ FILE: examples/v1/counter-with-create-react-app-ts/frontend/tsconfig.json ================================================ { "compilerOptions": { "target": "es5", "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react", "experimentalDecorators": true, "skipLibCheck": true }, "include": [ "src" ] } ================================================ FILE: examples/v1/counter-with-create-react-app-ts/package.json ================================================ { "name": "counter-with-create-react-app-ts", "version": "1.0.0", "private": true, "author": "Manuel Vila ", "license": "MIT", "scripts": { "postinstall": "(cd ./frontend && npm install) && (cd ./backend && npm install)", "start": "concurrently --names=frontend,backend --prefix-colors=green,blue --kill-others \"(cd ./frontend && npm run start)\" \"(cd ./backend && npm run start)\"", "update": "(cd ./frontend && npm update) && (cd ./backend && npm update)" }, "devDependencies": { "concurrently": "^5.3.0" } } ================================================ FILE: examples/v1/counter-with-esbuild-ts/README.md ================================================ # Counter with esbuild (TS) A simple example to introduce the core concepts of Layr. ## Install Install the npm dependencies with: ```sh npm install ``` ## Usage ### Running the app in development mode Execute the following command: ```sh npm run start ``` The app should then be available at http://localhost:3000. ### Debugging #### Client Add the following entry in the local storage of your browser: ``` | Key | Value | | ----- | --------- | | debug | layr:* | ``` #### Server Add the following environment variables when starting the app: ```sh DEBUG=layr:* DEBUG_DEPTH=10 ``` ================================================ FILE: examples/v1/counter-with-esbuild-ts/backend/package.json ================================================ { "name": "counter-with-esbuild-ts-backend", "version": "1.0.0", "private": true, "author": "Manuel Vila ", "license": "MIT", "scripts": { "build": "esbuild ./src/http-server.ts --bundle --target=node12 --sourcemap --platform=node --external:koa --outfile=./build/bundle.js", "start": "nodemon --watch ./src --ext ts --exec 'npm run build && node -r source-map-support/register ./build/bundle.js'" }, "dependencies": { "@layr/component": "^1.1.2", "@layr/component-server": "^1.1.2", "tslib": "^2.1.0" }, "devDependencies": { "@layr/component-http-server": "^1.1.2", "@types/node": "^14.14.20", "esbuild": "^0.9.2", "nodemon": "^2.0.7", "source-map-support": "^0.5.19", "typescript": "^4.1.3" } } ================================================ FILE: examples/v1/counter-with-esbuild-ts/backend/src/components/counter.ts ================================================ import {Component, primaryIdentifier, attribute, method, expose} from '@layr/component'; export class Counter extends Component { @expose({get: true, set: true}) @primaryIdentifier() id!: string; @expose({get: true, set: true}) @attribute('number') value = 0; @expose({call: true}) @method() async increment() { this.value++; } } ================================================ FILE: examples/v1/counter-with-esbuild-ts/backend/src/http-server.ts ================================================ import {ComponentHTTPServer} from '@layr/component-http-server'; import {server} from './server'; const httpServer = new ComponentHTTPServer(server, {port: 3001}); httpServer.start(); ================================================ FILE: examples/v1/counter-with-esbuild-ts/backend/src/server.ts ================================================ import {ComponentServer} from '@layr/component-server'; import {Counter} from './components/counter'; export const server = new ComponentServer(Counter); ================================================ FILE: examples/v1/counter-with-esbuild-ts/backend/tsconfig.json ================================================ { "include": ["src/**/*"], "compilerOptions": { "target": "ES2017", "module": "CommonJS", "lib": ["ESNext", "DOM"], "jsx": "react", "sourceMap": true, "importHelpers": true, "strict": true, "moduleResolution": "node", "esModuleInterop": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "experimentalDecorators": true, "resolveJsonModule": true, "stripInternal": true, "skipLibCheck": true, "isolatedModules": true } } ================================================ FILE: examples/v1/counter-with-esbuild-ts/frontend/package.json ================================================ { "name": "counter-with-esbuild-ts-frontend", "version": "1.0.0", "private": true, "author": "Manuel Vila ", "license": "MIT", "scripts": { "copy": "mkdir -p ./build && cp ./src/index.html ./build/index.html", "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", "serve": "serve --listen 3000 ./build", "start": "npm-run-all --parallel watch serve" }, "dependencies": { "@layr/component": "^1.1.2", "@layr/component-http-client": "^1.1.1", "@layr/react-integration": "^1.0.22", "react": "^16.13.1", "react-dom": "^16.13.1" }, "devDependencies": { "@types/node": "^14.14.20", "@types/react": "^16.14.2", "@types/react-dom": "^16.9.10", "esbuild": "^0.9.2", "npm-run-all": "^4.1.5", "serve": "^11.3.2" } } ================================================ FILE: examples/v1/counter-with-esbuild-ts/frontend/src/components/counter.tsx ================================================ import {ComponentHTTPClient} from '@layr/component-http-client'; import React from 'react'; import {view} from '@layr/react-integration'; import type {Counter as BackendCounter} from '../../../backend/src/components/counter'; export const getCounter = async () => { const client = new ComponentHTTPClient('http://localhost:3001'); const BackendCounterProxy = (await client.getComponent()) as typeof BackendCounter; class Counter extends BackendCounterProxy { @view() Main() { return (
); } } return Counter; }; ================================================ FILE: examples/v1/counter-with-esbuild-ts/frontend/src/index.html ================================================ My Rollup Project
================================================ FILE: examples/v1/counter-with-esbuild-ts/frontend/src/index.tsx ================================================ import React from 'react'; import ReactDOM from 'react-dom'; import {getCounter} from './components/counter'; (async () => { const Counter = await getCounter(); const counter = new Counter(); ReactDOM.render( , document.getElementById('root') ); })(); ================================================ FILE: examples/v1/counter-with-esbuild-ts/frontend/tsconfig.json ================================================ { "include": ["src/**/*"], "compilerOptions": { "target": "ES2017", "module": "CommonJS", "lib": ["ESNext", "DOM"], "jsx": "react", "sourceMap": true, "importHelpers": true, "strict": true, "moduleResolution": "node", "esModuleInterop": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "experimentalDecorators": true, "resolveJsonModule": true, "stripInternal": true, "skipLibCheck": true, "isolatedModules": true } } ================================================ FILE: examples/v1/counter-with-esbuild-ts/package.json ================================================ { "name": "counter-with-rollup-ts", "version": "1.0.0", "private": true, "author": "Manuel Vila ", "license": "MIT", "scripts": { "postinstall": "(cd ./frontend && npm install) && (cd ./backend && npm install)", "start": "concurrently --names=frontend,backend --prefix-colors=green,blue --kill-others \"(cd ./frontend && npm run start)\" \"(cd ./backend && npm run start)\"", "update": "(cd ./frontend && npm update) && (cd ./backend && npm update)" }, "devDependencies": { "concurrently": "^5.3.0" } } ================================================ FILE: examples/v1/counter-with-http/README.md ================================================ # Counter with HTTP A simple example to introduce the core concepts of Layr. ## Usage Install the npm dependencies with: ```sh npm install ``` Run the backend with: ```sh npx babel-node ./src/backend.js ``` Then, in another terminal, run the frontend with: ```sh npx babel-node ./src/frontend.js ``` ================================================ FILE: examples/v1/counter-with-http/babel.config.json ================================================ { "presets": [["@babel/preset-env", {"targets": {"node": "10"}}]], "plugins": [ ["@babel/plugin-proposal-decorators", {"legacy": true}], ["@babel/plugin-proposal-class-properties"] ] } ================================================ FILE: examples/v1/counter-with-http/jsconfig.json ================================================ { "compilerOptions": { "target": "ES2017", "module": "CommonJS", "checkJs": false, "experimentalDecorators": true } } ================================================ FILE: examples/v1/counter-with-http/package.json ================================================ { "name": "counter-with-http", "version": "1.0.0", "description": "A simple example to introduce the core concepts of Layr", "private": true, "dependencies": { "@layr/component": "^1.0.0", "@layr/component-http-client": "^1.0.0", "@layr/component-http-server": "^1.0.0" }, "devDependencies": { "@babel/core": "^7.11.1", "@babel/node": "^7.10.5", "@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/plugin-proposal-decorators": "^7.10.5", "@babel/preset-env": "^7.11.0" } } ================================================ FILE: examples/v1/counter-with-http/src/backend.js ================================================ import {Component, primaryIdentifier, attribute, method, expose} from '@layr/component'; import {ComponentHTTPServer} from '@layr/component-http-server'; class Counter extends Component { // We need a primary identifier so a Counter instance // can be transported between the frontend and the backend // while keeping it's identity @expose({get: true, set: true}) @primaryIdentifier() id; // The counter's value is exposed to the frontend @expose({get: true, set: true}) @attribute('number') value = 0; // And the "business logic" is exposed as well @expose({call: true}) @method() increment() { this.value++; } } // We serve the Counter through a ComponentHTTPServer const server = new ComponentHTTPServer(Counter, {port: 3210}); server.start(); ================================================ FILE: examples/v1/counter-with-http/src/frontend.js ================================================ import {ComponentHTTPClient} from '@layr/component-http-client'; (async () => { // We create a client that is connected to the backend's server const client = new ComponentHTTPClient('http://localhost:3210'); // We get the backend's Counter component const BackendCounter = await client.getComponent(); // We extends the backend's Counter component so we can override the increment() method class Counter extends BackendCounter { async increment() { await super.increment(); // The backend's `increment()` method is invoked console.log(this.value); // Some additional code is executed in the frontend } } // Lastly, we consume the Counter const counter = new Counter(); await counter.increment(); })(); ================================================ FILE: examples/v1/counter-with-parcel-ts/README.md ================================================ # Counter with Parcel (TS) A simple example to introduce the core concepts of Layr. ## Install Install the npm dependencies with: ```sh npm install ``` ## Usage ### Running the app in development mode Execute the following command: ```sh npm run start ``` The app should then be available at http://localhost:1234. ### Debugging #### Client Add the following entry in the local storage of your browser: ``` | Key | Value | | ----- | --------- | | debug | layr:* | ``` #### Server Add the following environment variables when starting the app: ```sh DEBUG=layr:* DEBUG_DEPTH=10 ``` ================================================ FILE: examples/v1/counter-with-parcel-ts/backend/package.json ================================================ { "name": "counter-with-parcel-ts-backend", "version": "1.0.0", "private": true, "author": "Manuel Vila ", "license": "MIT", "scripts": { "start": "nodemon --watch ./src --exec ts-node ./src/http-server.ts" }, "dependencies": { "@layr/component": "^1.1.2", "@layr/component-server": "^1.1.2", "tslib": "^2.1.0" }, "devDependencies": { "@layr/component-http-server": "^1.1.2", "@types/node": "^14.14.20", "nodemon": "^2.0.5", "ts-node": "^9.1.1", "typescript": "^4.1.3" } } ================================================ FILE: examples/v1/counter-with-parcel-ts/backend/src/components/counter.ts ================================================ import {Component, primaryIdentifier, attribute, method, expose} from '@layr/component'; export class Counter extends Component { @expose({get: true, set: true}) @primaryIdentifier() id!: string; @expose({get: true, set: true}) @attribute('number') value = 0; @expose({call: true}) @method() async increment() { this.value++; } } ================================================ FILE: examples/v1/counter-with-parcel-ts/backend/src/http-server.ts ================================================ import {ComponentHTTPServer} from '@layr/component-http-server'; import {server} from './server'; const httpServer = new ComponentHTTPServer(server, {port: 1235}); httpServer.start(); ================================================ FILE: examples/v1/counter-with-parcel-ts/backend/src/server.ts ================================================ import {ComponentServer} from '@layr/component-server'; import {Counter} from './components/counter'; export const server = new ComponentServer(Counter); ================================================ FILE: examples/v1/counter-with-parcel-ts/backend/tsconfig.json ================================================ { "include": ["src/**/*"], "compilerOptions": { "target": "ES2017", "module": "CommonJS", "lib": ["ESNext", "DOM"], "jsx": "react", "sourceMap": true, "importHelpers": true, "strict": true, "moduleResolution": "node", "esModuleInterop": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "experimentalDecorators": true, "resolveJsonModule": true, "stripInternal": true, "skipLibCheck": true } } ================================================ FILE: examples/v1/counter-with-parcel-ts/frontend/.gitignore ================================================ /.parcel-cache ================================================ FILE: examples/v1/counter-with-parcel-ts/frontend/babel.config.js ================================================ module.exports = (api) => { api.cache(true); const presets = [ ['@babel/preset-typescript'], [ '@babel/preset-env', { targets: {chrome: '55', safari: '11', firefox: '54'}, loose: true, modules: false } ], ['@babel/preset-react'] ]; const plugins = [ ['@babel/plugin-proposal-decorators', {legacy: true}], ['@babel/plugin-proposal-class-properties', {loose: true}] ]; return {presets, plugins}; }; ================================================ FILE: examples/v1/counter-with-parcel-ts/frontend/package.json ================================================ { "name": "counter-with-parcel-ts-frontend", "version": "1.0.0", "private": true, "author": "Manuel Vila ", "license": "MIT", "scripts": { "start": "parcel serve ./src/index.html" }, "dependencies": { "@layr/component": "^1.1.2", "@layr/component-http-client": "^1.1.1", "@layr/react-integration": "^1.0.22", "react": "^16.13.1", "react-dom": "^16.13.1" }, "devDependencies": { "@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/plugin-proposal-decorators": "^7.12.12", "@babel/preset-env": "^7.12.11", "@babel/preset-react": "^7.12.10", "@babel/preset-typescript": "^7.12.7", "@types/node": "^14.14.20", "@types/react": "^16.14.2", "@types/react-dom": "^16.9.10", "parcel": "next" } } ================================================ FILE: examples/v1/counter-with-parcel-ts/frontend/src/components/counter.tsx ================================================ import {ComponentHTTPClient} from '@layr/component-http-client'; import React from 'react'; import {view} from '@layr/react-integration'; import type {Counter as BackendCounter} from '../../../backend/src/components/counter'; export const getCounter = async () => { const client = new ComponentHTTPClient('http://localhost:1235'); const BackendCounterProxy = (await client.getComponent()) as typeof BackendCounter; class Counter extends BackendCounterProxy { @view() Main() { return (
); } } return Counter; }; ================================================ FILE: examples/v1/counter-with-parcel-ts/frontend/src/index.html ================================================ My Parcel Project
================================================ FILE: examples/v1/counter-with-parcel-ts/frontend/src/index.tsx ================================================ import React from 'react'; import ReactDOM from 'react-dom'; import {getCounter} from './components/counter'; (async () => { const Counter = await getCounter(); const counter = new Counter(); ReactDOM.render( , document.getElementById('root') ); })(); ================================================ FILE: examples/v1/counter-with-parcel-ts/frontend/tsconfig.json ================================================ { "include": ["src/**/*"], "compilerOptions": { "target": "ES2017", "module": "CommonJS", "lib": ["ESNext", "DOM"], "jsx": "react", "sourceMap": true, "importHelpers": true, "strict": true, "moduleResolution": "node", "esModuleInterop": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "experimentalDecorators": true, "resolveJsonModule": true, "stripInternal": true, "skipLibCheck": true } } ================================================ FILE: examples/v1/counter-with-parcel-ts/package.json ================================================ { "name": "counter-with-parcel-ts", "version": "1.0.0", "private": true, "author": "Manuel Vila ", "license": "MIT", "scripts": { "postinstall": "(cd ./frontend && npm install) && (cd ./backend && npm install)", "start": "concurrently --names=frontend,backend --prefix-colors=green,blue --kill-others \"(cd ./frontend && npm run start)\" \"(cd ./backend && npm run start)\"", "update": "(cd ./frontend && npm update) && (cd ./backend && npm update)" }, "devDependencies": { "concurrently": "^5.3.0" } } ================================================ FILE: examples/v1/counter-with-rollup-ts/README.md ================================================ # Counter with Rollup (TS) A simple example to introduce the core concepts of Layr. ## Install Install the npm dependencies with: ```sh npm install ``` ## Usage ### Running the app in development mode Execute the following command: ```sh npm run start ``` The app should then be available at http://localhost:3000. ### Debugging #### Client Add the following entry in the local storage of your browser: ``` | Key | Value | | ----- | --------- | | debug | layr:* | ``` #### Server Add the following environment variables when starting the app: ```sh DEBUG=layr:* DEBUG_DEPTH=10 ``` ================================================ FILE: examples/v1/counter-with-rollup-ts/backend/package.json ================================================ { "name": "counter-with-rollup-ts-backend", "version": "1.0.0", "private": true, "author": "Manuel Vila ", "license": "MIT", "scripts": { "start": "nodemon --watch ./src --exec ts-node ./src/http-server.ts" }, "dependencies": { "@layr/component": "^1.1.2", "@layr/component-server": "^1.1.2", "tslib": "^2.1.0" }, "devDependencies": { "@layr/component-http-server": "^1.1.2", "@types/node": "^14.14.20", "nodemon": "^2.0.5", "ts-node": "^9.1.1", "typescript": "^4.1.3" } } ================================================ FILE: examples/v1/counter-with-rollup-ts/backend/src/components/counter.ts ================================================ import {Component, primaryIdentifier, attribute, method, expose} from '@layr/component'; export class Counter extends Component { @expose({get: true, set: true}) @primaryIdentifier() id!: string; @expose({get: true, set: true}) @attribute('number') value = 0; @expose({call: true}) @method() async increment() { this.value++; } } ================================================ FILE: examples/v1/counter-with-rollup-ts/backend/src/http-server.ts ================================================ import {ComponentHTTPServer} from '@layr/component-http-server'; import {server} from './server'; const httpServer = new ComponentHTTPServer(server, {port: 3001}); httpServer.start(); ================================================ FILE: examples/v1/counter-with-rollup-ts/backend/src/server.ts ================================================ import {ComponentServer} from '@layr/component-server'; import {Counter} from './components/counter'; export const server = new ComponentServer(Counter); ================================================ FILE: examples/v1/counter-with-rollup-ts/backend/tsconfig.json ================================================ { "include": ["src/**/*"], "compilerOptions": { "target": "ES2017", "module": "CommonJS", "lib": ["ESNext", "DOM"], "jsx": "react", "sourceMap": true, "importHelpers": true, "strict": true, "moduleResolution": "node", "esModuleInterop": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "experimentalDecorators": true, "resolveJsonModule": true, "stripInternal": true, "skipLibCheck": true } } ================================================ FILE: examples/v1/counter-with-rollup-ts/frontend/babel.config.js ================================================ module.exports = (api) => { api.cache(true); const presets = [ ['@babel/preset-typescript'], [ '@babel/preset-env', { targets: {chrome: '55', safari: '11', firefox: '54'}, loose: true, modules: false } ], ['@babel/preset-react'] ]; const plugins = [ ['@babel/plugin-proposal-decorators', {legacy: true}], ['@babel/plugin-proposal-class-properties', {loose: true}] ]; return {presets, plugins}; }; ================================================ FILE: examples/v1/counter-with-rollup-ts/frontend/package.json ================================================ { "name": "counter-with-rollup-ts-frontend", "version": "1.0.0", "private": true, "author": "Manuel Vila ", "license": "MIT", "scripts": { "copy": "mkdir -p ./build && cp ./src/index.html ./build/index.html", "build": "npm run copy && rollup --config", "watch": "npm run copy && rollup --config --watch", "serve": "serve --listen 3000 ./build", "start": "npm-run-all --parallel watch serve" }, "dependencies": { "@layr/component": "^1.1.2", "@layr/component-http-client": "^1.1.1", "@layr/react-integration": "^1.0.22", "react": "^16.13.1", "react-dom": "^16.13.1" }, "devDependencies": { "@babel/core": "^7.12.10", "@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/plugin-proposal-decorators": "^7.12.12", "@babel/preset-env": "^7.12.11", "@babel/preset-react": "^7.12.10", "@babel/preset-typescript": "^7.12.7", "@rollup/plugin-babel": "^5.2.2", "@rollup/plugin-commonjs": "^15.1.0", "@rollup/plugin-node-resolve": "^9.0.0", "@types/node": "^14.14.20", "@types/react": "^16.14.2", "@types/react-dom": "^16.9.10", "npm-run-all": "^4.1.5", "rollup": "^2.36.0", "rollup-plugin-node-globals": "^1.4.0", "serve": "^11.3.2" } } ================================================ FILE: examples/v1/counter-with-rollup-ts/frontend/rollup.config.js ================================================ import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import babel from '@rollup/plugin-babel'; import globals from 'rollup-plugin-node-globals'; export default { input: 'src/index.tsx', output: { file: 'build/bundle.js', format: 'iife', sourcemap: true }, plugins: [ resolve({browser: true, extensions: ['.mjs', '.js', '.json', '.node', '.ts', '.tsx']}), commonjs(), globals(), babel({extensions: ['ts', 'tsx'], babelHelpers: 'bundled'}) ] }; ================================================ FILE: examples/v1/counter-with-rollup-ts/frontend/src/components/counter.tsx ================================================ import {ComponentHTTPClient} from '@layr/component-http-client'; import React from 'react'; import {view} from '@layr/react-integration'; import type {Counter as BackendCounter} from '../../../backend/src/components/counter'; export const getCounter = async () => { const client = new ComponentHTTPClient('http://localhost:3001'); const BackendCounterProxy = (await client.getComponent()) as typeof BackendCounter; class Counter extends BackendCounterProxy { @view() Main() { return (
); } } return Counter; }; ================================================ FILE: examples/v1/counter-with-rollup-ts/frontend/src/index.html ================================================ My Rollup Project
================================================ FILE: examples/v1/counter-with-rollup-ts/frontend/src/index.tsx ================================================ import React from 'react'; import ReactDOM from 'react-dom'; import {getCounter} from './components/counter'; (async () => { const Counter = await getCounter(); const counter = new Counter(); ReactDOM.render( , document.getElementById('root') ); })(); ================================================ FILE: examples/v1/counter-with-rollup-ts/frontend/tsconfig.json ================================================ { "include": ["src/**/*"], "compilerOptions": { "target": "ES2017", "module": "CommonJS", "lib": ["ESNext", "DOM"], "jsx": "react", "sourceMap": true, "importHelpers": true, "strict": true, "moduleResolution": "node", "esModuleInterop": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "experimentalDecorators": true, "resolveJsonModule": true, "stripInternal": true, "skipLibCheck": true } } ================================================ FILE: examples/v1/counter-with-rollup-ts/package.json ================================================ { "name": "counter-with-rollup-ts", "version": "1.0.0", "private": true, "author": "Manuel Vila ", "license": "MIT", "scripts": { "postinstall": "(cd ./frontend && npm install) && (cd ./backend && npm install)", "start": "concurrently --names=frontend,backend --prefix-colors=green,blue --kill-others \"(cd ./frontend && npm run start)\" \"(cd ./backend && npm run start)\"", "update": "(cd ./frontend && npm update) && (cd ./backend && npm update)" }, "devDependencies": { "concurrently": "^5.3.0" } } ================================================ FILE: examples/v1/guestbook-cli-js/README.md ================================================ # Guestbook CLI (JS) A simple CLI app to introduce data storage with Layr. See the [corresponding guide](https://layrjs.com/docs/v1/introduction/data-storage?language=js). ================================================ FILE: examples/v1/guestbook-cli-js/babel.config.json ================================================ { "presets": [["@babel/preset-env", {"targets": {"node": "10"}}]], "plugins": [ ["@babel/plugin-proposal-decorators", {"legacy": true}], ["@babel/plugin-proposal-class-properties"] ] } ================================================ FILE: examples/v1/guestbook-cli-js/jsconfig.json ================================================ { "compilerOptions": { "target": "ES2017", "module": "CommonJS", "checkJs": false, "experimentalDecorators": true } } ================================================ FILE: examples/v1/guestbook-cli-js/package.json ================================================ { "name": "guestbook-cli", "version": "1.0.0", "description": "A simple CLI app to introduce data storage with Layr", "private": true, "dependencies": { "@layr/component": "^1.0.0", "@layr/component-http-client": "^1.0.0", "@layr/component-http-server": "^1.0.0", "@layr/memory-store": "^1.0.0", "@layr/storable": "^1.0.0" }, "devDependencies": { "@babel/core": "^7.11.1", "@babel/node": "^7.10.5", "@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/plugin-proposal-decorators": "^7.10.5", "@babel/preset-env": "^7.11.0" } } ================================================ FILE: examples/v1/guestbook-cli-js/src/backend.js ================================================ import {Component, expose, validators} from '@layr/component'; import {Storable, primaryIdentifier, attribute} from '@layr/storable'; import {MemoryStore} from '@layr/memory-store'; import {ComponentHTTPServer} from '@layr/component-http-server'; const {notEmpty, maxLength} = validators; @expose({ find: {call: true}, prototype: { load: {call: true}, save: {call: true} } }) export class Message extends Storable(Component) { @expose({get: true, set: true}) @primaryIdentifier() id; @expose({get: true, set: true}) @attribute('string', {validators: [notEmpty(), maxLength(300)]}) text = ''; @expose({get: true}) @attribute('Date') createdAt = new Date(); } const store = new MemoryStore(); store.registerStorable(Message); const server = new ComponentHTTPServer(Message, {port: 3210}); server.start(); ================================================ FILE: examples/v1/guestbook-cli-js/src/frontend.js ================================================ import {ComponentHTTPClient} from '@layr/component-http-client'; import {Storable} from '@layr/storable'; (async () => { const client = new ComponentHTTPClient('http://localhost:3210', { mixins: [Storable] }); const Message = await client.getComponent(); const text = process.argv[2]; if (text) { addMessage(text); } else { showMessages(); } async function addMessage(text) { const message = new Message({text}); await message.save(); console.log(`Message successfully added`); } async function showMessages() { const messages = await Message.find( {}, {text: true, createdAt: true}, {sort: {createdAt: 'desc'}, limit: 30} ); for (const message of messages) { console.log(`[${message.createdAt.toISOString()}] ${message.text}`); } } })(); ================================================ FILE: examples/v1/guestbook-cli-ts/README.md ================================================ # Guestbook CLI (TS) A simple CLI app to introduce data storage with Layr. See the [corresponding guide](https://layrjs.com/docs/v1/introduction/data-storage?language=ts). ================================================ FILE: examples/v1/guestbook-cli-ts/package.json ================================================ { "name": "guestbook-cli", "version": "1.0.0", "description": "A simple CLI app to introduce data storage with Layr", "private": true, "dependencies": { "@layr/component": "^1.0.0", "@layr/component-http-client": "^1.0.0", "@layr/component-http-server": "^1.0.0", "@layr/memory-store": "^1.0.0", "@layr/storable": "^1.0.0" }, "devDependencies": { "ts-node": "^8.10.2", "typescript": "^3.9.7" } } ================================================ FILE: examples/v1/guestbook-cli-ts/src/backend.ts ================================================ import {Component, expose, validators} from '@layr/component'; import {Storable, primaryIdentifier, attribute} from '@layr/storable'; import {MemoryStore} from '@layr/memory-store'; import {ComponentHTTPServer} from '@layr/component-http-server'; const {notEmpty, maxLength} = validators; @expose({ find: {call: true}, prototype: { load: {call: true}, save: {call: true} } }) export class Message extends Storable(Component) { @expose({get: true, set: true}) @primaryIdentifier() id!: string; @expose({get: true, set: true}) @attribute('string', {validators: [notEmpty(), maxLength(300)]}) text = ''; @expose({get: true}) @attribute('Date') createdAt = new Date(); } const store = new MemoryStore(); store.registerStorable(Message); const server = new ComponentHTTPServer(Message, {port: 3210}); server.start(); ================================================ FILE: examples/v1/guestbook-cli-ts/src/frontend.ts ================================================ import {ComponentHTTPClient} from '@layr/component-http-client'; import {Storable} from '@layr/storable'; import type {Message as MessageType} from './backend'; (async () => { const client = new ComponentHTTPClient('http://localhost:3210', { mixins: [Storable] }); const Message = (await client.getComponent()) as typeof MessageType; const text = process.argv[2]; if (text) { addMessage(text); } else { showMessages(); } async function addMessage(text: string) { const message = new Message({text}); await message.save(); console.log(`Message successfully added`); } async function showMessages() { const messages = await Message.find( {}, {text: true, createdAt: true}, {sort: {createdAt: 'desc'}, limit: 30} ); for (const message of messages) { console.log(`[${message.createdAt.toISOString()}] ${message.text}`); } } })(); ================================================ FILE: examples/v1/guestbook-cli-ts/tsconfig.json ================================================ { "include": ["src/**/*"], "compilerOptions": { "target": "ES2017", "module": "CommonJS", "lib": ["ESNext", "DOM"], "sourceMap": true, "strict": true, "moduleResolution": "node", "esModuleInterop": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "experimentalDecorators": true, "skipLibCheck": true } } ================================================ FILE: examples/v1/guestbook-web-js/README.md ================================================ # Guestbook Web (JS) A simple web app showing how to build a web frontend with Layr. See the [corresponding guide](https://layrjs.com/docs/v1/introduction/web-app?language=js). ================================================ FILE: examples/v1/guestbook-web-js/babel.config.json ================================================ { "presets": [["@babel/preset-env", {"targets": {"node": "10"}}], "@babel/preset-react"], "plugins": [ ["@babel/plugin-proposal-decorators", {"legacy": true}], ["@babel/plugin-proposal-class-properties"] ] } ================================================ FILE: examples/v1/guestbook-web-js/jsconfig.json ================================================ { "compilerOptions": { "target": "ES2017", "module": "CommonJS", "checkJs": false, "experimentalDecorators": true } } ================================================ FILE: examples/v1/guestbook-web-js/package.json ================================================ { "name": "guestbook-web", "version": "1.0.0", "description": "A simple web app showing how to build a web frontend with Layr", "private": true, "dependencies": { "@layr/component": "^1.0.0", "@layr/component-http-client": "^1.0.0", "@layr/component-http-server": "^1.0.0", "@layr/memory-store": "^1.0.0", "@layr/react-integration": "^1.0.0", "@layr/storable": "^1.0.0", "react": "^16.13.1", "react-dom": "^16.13.1" }, "devDependencies": { "@babel/core": "^7.11.5", "@babel/node": "^7.10.5", "@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/plugin-proposal-decorators": "^7.10.5", "@babel/preset-env": "^7.11.5", "@babel/preset-react": "^7.10.4", "babel-loader": "^8.1.0", "html-webpack-plugin": "^4.4.1", "webpack": "^4.44.1", "webpack-cli": "^3.3.12", "webpack-dev-server": "^3.11.0" } } ================================================ FILE: examples/v1/guestbook-web-js/src/backend.js ================================================ import {Component, expose, validators} from '@layr/component'; import {Storable, primaryIdentifier, attribute} from '@layr/storable'; import {MemoryStore} from '@layr/memory-store'; import {ComponentHTTPServer} from '@layr/component-http-server'; const {notEmpty, maxLength} = validators; @expose({ find: {call: true}, prototype: { load: {call: true}, save: {call: true} } }) export class Message extends Storable(Component) { @expose({get: true, set: true}) @primaryIdentifier() id; @expose({get: true, set: true}) @attribute('string', {validators: [notEmpty(), maxLength(300)]}) text = ''; @expose({get: true}) @attribute('Date') createdAt = new Date(); } const store = new MemoryStore(); store.registerStorable(Message); const server = new ComponentHTTPServer(Message, {port: 3210}); server.start(); ================================================ FILE: examples/v1/guestbook-web-js/src/frontend.js ================================================ import React, {useCallback} from 'react'; import ReactDOM from 'react-dom'; import {Component, attribute, provide} from '@layr/component'; import {Storable} from '@layr/storable'; import {ComponentHTTPClient} from '@layr/component-http-client'; import {view, useAsyncCall, useAsyncCallback, useRecomputableMemo} from '@layr/react-integration'; async function main() { const client = new ComponentHTTPClient('http://localhost:3210', { mixins: [Storable] }); const BackendMessage = await client.getComponent(); class Message extends BackendMessage { @view() Viewer() { return (
{this.createdAt.toLocaleString()}
{this.text}
); } @view() Form({onSubmit}) { const [handleSubmit, isSubmitting, submitError] = useAsyncCallback(async (event) => { event.preventDefault(); await onSubmit(); }); return (