From 4ca441f43a60beed9d3037fa49b11e665ba16e18 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 22 Sep 2022 09:44:21 +0200 Subject: [PATCH] Initial import Signed-off-by: Matteo Collina Co-authored-by: Luca Maraschi Co-authored-by: Leonardo Rossi Co-authored-by: Marco Piraccini Co-authored-by: Ivan Tymoshenko Co-authored-by: Simon Plenderleith --- .dockerignore | 112 + .github/workflows/ci.yml | 227 + .github/workflows/docker.yml | 29 + .github/workflows/issues.yml | 25 + .github/workflows/update-docs.yml | 23 + .gitignore | 112 + .npmrc | 3 + CONTRIBUTING.md | 94 + Dockerfile | 45 + LICENSE | 201 + NOTICE | 13 + README.md | 35 + demo/auth/migrations/001.do.sql | 4 + demo/auth/migrations/001.undo.sql | 1 + demo/auth/migrations/002.do.sql | 5 + demo/auth/migrations/002.undo.sql | 2 + demo/auth/migrations/003.do.sql | 1 + demo/auth/migrations/003.undo.sql | 1 + demo/auth/platformatic.db.json | 51 + demo/auth/plugin.js | 34 + demo/basic/migrations/001.do.sql | 4 + demo/basic/migrations/001.undo.sql | 1 + demo/basic/migrations/002.do.sql | 5 + demo/basic/migrations/002.undo.sql | 2 + demo/basic/platformatic.db.json | 18 + docker-compose-apple-silicon.yml | 33 + docker-compose-mac.yml | 32 + docker-compose.yml | 32 + docs/contributing/contributing.md | 3 + .../contributing/documentation-style-guide.md | 238 + docs/getting-started/architecture.md | 28 + .../movie-quotes-app-tutorial.md | 1888 +++++ .../platformatic-architecture.png | Bin 0 -> 512507 bytes .../platformatid-db-architecture.excalidraw | 1777 ++++ docs/getting-started/quick-start-guide.md | 228 + .../extend-graphql.md | 117 + .../add-custom-functionality/extend-rest.md | 88 + .../add-custom-functionality/introduction.md | 49 + .../add-custom-functionality/prerequisites.md | 110 + docs/guides/deployment.md | 218 + docs/guides/jwt-auth0.md | 31 + docs/guides/seed-a-database.md | 30 + docs/reference/cli.md | 292 + docs/reference/configuration.md | 391 + .../db-authorization/images/http.png | Bin 0 -> 80192 bytes .../reference/db-authorization/images/jwt.png | Bin 0 -> 116969 bytes .../images/sources/http.excalidraw | 242 + .../images/sources/jwt.excalidraw | 477 ++ .../images/sources/webhook.excalidraw | 481 ++ .../db-authorization/images/webhook.png | Bin 0 -> 120171 bytes .../db-authorization/introduction.md | 123 + .../db-authorization/programmatic-rules.md | 81 + docs/reference/migrations.md | 62 + docs/reference/plugin.md | 39 + .../sql-graphql/examples/deleteEntity.js | 38 + .../sql-graphql/examples/insertEntity.js | 38 + docs/reference/sql-graphql/examples/query.js | 37 + .../sql-graphql/examples/saveEntity.js | 38 + docs/reference/sql-graphql/introduction.md | 17 + docs/reference/sql-graphql/mutations.md | 152 + docs/reference/sql-graphql/queries.md | 77 + docs/reference/sql-mapper/entities/api.md | 244 + docs/reference/sql-mapper/entities/example.md | 160 + docs/reference/sql-mapper/entities/fields.md | 91 + docs/reference/sql-mapper/entities/hooks.md | 144 + .../sql-mapper/entities/introduction.md | 11 + .../sql-mapper/entities/relations.md | 87 + docs/reference/sql-mapper/examples/delete.js | 30 + .../sql-mapper/examples/fastify-plugin.js | 25 + docs/reference/sql-mapper/examples/fields.js | 19 + docs/reference/sql-mapper/examples/find.js | 30 + docs/reference/sql-mapper/examples/hooks.js | 46 + docs/reference/sql-mapper/examples/insert.js | 25 + .../sql-mapper/examples/relations.js | 38 + docs/reference/sql-mapper/examples/save.js | 21 + docs/reference/sql-mapper/fastify-plugin.md | 42 + .../sql-mapper/images/plt-db-hooks.svg | 4 + docs/reference/sql-mapper/introduction.md | 51 + docs/reference/sql-rest/api.md | 149 + docs/reference/sql-rest/introduction.md | 23 + package.json | 19 + packages/authenticate/LICENSE | 201 + packages/authenticate/NOTICE | 13 + packages/authenticate/README.md | 27 + packages/authenticate/authenticate.js | 26 + packages/authenticate/lib/login.js | 158 + packages/authenticate/lib/schema.js | 7 + packages/authenticate/lib/utils.js | 3 + packages/authenticate/package.json | 35 + packages/authenticate/test/login.test.js | 366 + packages/cli/.npmignore | 2 + packages/cli/.taprc | 1 + packages/cli/LICENSE | 201 + packages/cli/NOTICE | 13 + packages/cli/README.md | 35 + packages/cli/cli.js | 57 + packages/cli/help/help.txt | 5 + packages/cli/lib/ascii.js | 36 + packages/cli/package.json | 36 + packages/cli/test/helper.js | 7 + packages/cli/test/platformatic.test.js | 60 + packages/config/LICENSE | 201 + packages/config/NOTICE | 13 + packages/config/README.md | 18 + packages/config/index.d.ts | 42 + packages/config/index.js | 252 + packages/config/lib/plugin.js | 62 + packages/config/package.json | 35 + .../config/test/fixtures/bad-placeholder.json | 18 + packages/config/test/fixtures/simple.json | 36 + packages/config/test/fixtures/simple.json5 | 41 + packages/config/test/fixtures/simple.toml | 33 + packages/config/test/fixtures/simple.yaml | 26 + packages/config/test/fixtures/sqlite.json | 16 + packages/config/test/helper.js | 18 + packages/config/test/index.test.js | 225 + packages/config/test/load.test.js | 110 + packages/config/test/placeholders.test.js | 85 + packages/config/test/plugin.test.js | 71 + packages/config/test/save.test.js | 333 + packages/config/test/watch.test.js | 294 + packages/db-authorization/.npmignore | 2 + packages/db-authorization/.taprc | 1 + packages/db-authorization/LICENSE | 201 + packages/db-authorization/NOTICE | 13 + packages/db-authorization/README.md | 15 + packages/db-authorization/index.js | 293 + packages/db-authorization/lib/find-rule.d.ts | 14 + packages/db-authorization/lib/find-rule.js | 19 + packages/db-authorization/lib/jwt.js | 30 + packages/db-authorization/lib/utils.d.ts | 5 + packages/db-authorization/lib/utils.js | 32 + packages/db-authorization/lib/webhook.js | 41 + packages/db-authorization/package.json | 33 + packages/db-authorization/schema.js | 15 + packages/db-authorization/test/admin.test.js | 1150 +++ .../test/code-permissions.test.js | 647 ++ .../test/fields-permissions.test.js | 597 ++ .../db-authorization/test/find-rule.test.js | 95 + packages/db-authorization/test/helper.js | 58 + packages/db-authorization/test/jwt.test.js | 515 ++ .../db-authorization/test/multi-role.test.js | 483 ++ packages/db-authorization/test/nested.test.js | 220 + .../test/set-from-code.test.js | 487 ++ .../db-authorization/test/simple-rest.test.js | 228 + packages/db-authorization/test/simple.test.js | 1439 ++++ packages/db-authorization/test/utils.test.js | 61 + .../db-authorization/test/webhook.test.js | 719 ++ packages/db-core/.npmignore | 2 + packages/db-core/.taprc | 1 + packages/db-core/LICENSE | 201 + packages/db-core/NOTICE | 13 + packages/db-core/README.md | 32 + packages/db-core/index.js | 29 + packages/db-core/package.json | 32 + packages/db-core/test/basic.test.js | 161 + packages/db-core/test/helper.js | 69 + packages/db-dashboard/.gitignore | 24 + packages/db-dashboard/LICENSE | 201 + packages/db-dashboard/NOTICE | 13 + packages/db-dashboard/README.md | 8 + packages/db-dashboard/index.html | 13 + packages/db-dashboard/index.js | 20 + packages/db-dashboard/package.json | 69 + packages/db-dashboard/playwright.config.js | 72 + .../public/images/apple-touch-icon.png | Bin 0 -> 9591 bytes .../public/images/favicon-16x16.png | Bin 0 -> 843 bytes .../public/images/favicon-32x32.png | Bin 0 -> 1443 bytes .../db-dashboard/public/images/favicon.ico | Bin 0 -> 10990 bytes .../public/images/logo-192x192.png | Bin 0 -> 10314 bytes .../public/images/logo-512x512.png | Bin 0 -> 28278 bytes packages/db-dashboard/public/index.html | 43 + packages/db-dashboard/public/manifest.json | 25 + packages/db-dashboard/public/robots.txt | 3 + packages/db-dashboard/schema.js | 1 + packages/db-dashboard/src/App.css | 41 + packages/db-dashboard/src/App.jsx | 51 + packages/db-dashboard/src/assets/react.svg | 1 + .../db-dashboard/src/components/Layout.jsx | 56 + .../db-dashboard/src/components/LoginBox.jsx | 54 + .../db-dashboard/src/components/Navbar.jsx | 25 + .../src/components/Navbar.module.css | 3 + .../src/components/Navbar.test.jsx | 12 + .../src/components/Notification.jsx | 9 + .../db-dashboard/src/components/Sidebar.css | 3 + .../db-dashboard/src/components/Sidebar.jsx | 27 + .../src/components/Sidebar.test.jsx | 18 + packages/db-dashboard/src/elements/Title.jsx | 3 + packages/db-dashboard/src/index.css | 70 + packages/db-dashboard/src/main.jsx | 13 + .../db-dashboard/src/pages/ConfigViewer.jsx | 78 + packages/db-dashboard/src/pages/GQL.jsx | 48 + .../db-dashboard/src/pages/GQL.module.css | 13 + packages/db-dashboard/src/pages/Home.jsx | 28 + .../db-dashboard/src/pages/SwaggerViewer.jsx | 10 + packages/db-dashboard/src/reportWebVitals.js | 13 + packages/db-dashboard/src/setupTests.js | 5 + packages/db-dashboard/src/utils.js | 14 + .../test/e2e/fixtures/e2e-test-config.json | 35 + .../test/e2e/fixtures/migrations/001.do.sql | 4 + .../test/e2e/fixtures/migrations/001.undo.sql | 1 + .../test/e2e/fixtures/migrations/002.do.sql | 5 + .../test/e2e/fixtures/migrations/002.undo.sql | 2 + packages/db-dashboard/test/e2e/index.spec.js | 27 + packages/db-dashboard/vite.config.js | 10 + packages/db-dashboard/vitest.config.js | 11 + packages/db/.npmignore | 2 + packages/db/.taprc | 2 + packages/db/LICENSE | 201 + packages/db/NOTICE | 13 + packages/db/README.md | 13 + packages/db/_admin/auth-routes.js | 48 + packages/db/_admin/index.js | 24 + packages/db/_admin/non-auth-routes.js | 59 + packages/db/db.mjs | 64 + packages/db/fixtures/bad-migrations.json | 15 + .../db/fixtures/bad-migrations/001.do.sql | 4 + .../db/fixtures/bad-migrations/001.undo.sql | 1 + .../db/fixtures/bad-migrations/002.do.sql | 3 + packages/db/fixtures/migrations/001.do.sql | 4 + packages/db/fixtures/migrations/001.undo.sql | 1 + packages/db/fixtures/no-connectionString.json | 12 + packages/db/fixtures/no-migrations-dir.json | 6 + packages/db/fixtures/no-migrations.json | 15 + packages/db/fixtures/simple.json | 18 + .../db/fixtures/sqlite/migrations/001.do.sql | 4 + .../fixtures/sqlite/migrations/001.undo.sql | 1 + .../db/fixtures/sqlite/platformatic.db.json | 16 + packages/db/help/help.txt | 8 + packages/db/help/init.txt | 14 + packages/db/help/migrate.txt | 32 + packages/db/help/schema.txt | 13 + packages/db/help/seed.txt | 27 + packages/db/help/start.txt | 41 + packages/db/help/types.txt | 32 + packages/db/index.js | 158 + packages/db/lib/config.js | 63 + packages/db/lib/errors.mjs | 18 + packages/db/lib/gen-schema.mjs | 54 + packages/db/lib/gen-types.mjs | 162 + packages/db/lib/helper.js | 13 + packages/db/lib/init.mjs | 125 + packages/db/lib/load-config.mjs | 58 + packages/db/lib/metrics-plugin.js | 88 + packages/db/lib/migrate.mjs | 48 + packages/db/lib/migrator.mjs | 66 + packages/db/lib/root-endpoint/index.js | 21 + .../db/lib/root-endpoint/public/index.html | 99 + .../lib/root-endpoint/public/logo-512x512.png | Bin 0 -> 28278 bytes packages/db/lib/schema.js | 379 + packages/db/lib/seed.mjs | 61 + packages/db/lib/start.mjs | 108 + packages/db/lib/utils.js | 45 + packages/db/package.json | 68 + .../test/cli/env.test.mjs.test.cjs | 71 + .../test/cli/schema.test.mjs.test.cjs | 520 ++ packages/db/test/admin.test.js | 332 + packages/db/test/authorization-setup.test.js | 70 + packages/db/test/cli/env.test.mjs | 145 + packages/db/test/cli/gen-types.test.mjs | 89 + packages/db/test/cli/help.test.mjs | 19 + packages/db/test/cli/helper.mjs | 143 + packages/db/test/cli/init.test.mjs | 298 + .../test/cli/load-and-reload-files.test.mjs | 273 + packages/db/test/cli/migrate.test.mjs | 96 + packages/db/test/cli/schema.test.mjs | 35 + packages/db/test/cli/seed.test.mjs | 180 + packages/db/test/cli/sqlite3.test.mjs | 145 + packages/db/test/cli/start.test.mjs | 386 + packages/db/test/cli/validations.test.mjs | 33 + packages/db/test/cli/watch.test.mjs | 290 + packages/db/test/config.test.js | 254 + packages/db/test/cors.test.js | 55 + packages/db/test/fixtures/auto-apply.json | 17 + .../test/fixtures/auto-gen-types/index.d.ts | 1 + .../fixtures/auto-gen-types/index.test-d.ts | 14 + .../auto-gen-types/migrations/001.do.sql | 4 + .../test/fixtures/auto-gen-types/package.json | 16 + .../auto-gen-types/platformatic.db.json | 18 + .../test/fixtures/auto/migrations/001.do.sql | 4 + .../fixtures/auto/migrations/001.undo.sql | 1 + .../test/fixtures/auto/platformatic.db.json | 17 + .../test/fixtures/env-whitelist-default.json | 12 + packages/db/test/fixtures/env-whitelist.json | 12 + packages/db/test/fixtures/gen-types/db | Bin 0 -> 16384 bytes .../db/test/fixtures/gen-types/index.d.ts | 1 + .../test/fixtures/gen-types/index.test-d.ts | 14 + .../fixtures/gen-types/migrations/001.do.sql | 4 + .../db/test/fixtures/gen-types/package.json | 17 + .../fixtures/gen-types/platformatic.db.json | 16 + packages/db/test/fixtures/init/.gitkeep | 0 packages/db/test/fixtures/init/db.sqlite | Bin 0 -> 12288 bytes packages/db/test/fixtures/init/package.json | 13 + .../invalid-migrations-directory.json | 16 + .../db/test/fixtures/migrations/001.do.sql | 4 + .../db/test/fixtures/migrations/001.undo.sql | 1 + .../fixtures/missing-required-values.json | 9 + .../db/test/fixtures/no-server-logger.json | 14 + packages/db/test/fixtures/placeholder.json | 18 + .../db/test/fixtures/root-endpoint-plugin.js | 8 + packages/db/test/fixtures/simple.json | 17 + packages/db/test/fixtures/sqlite/ignore.json | 18 + .../fixtures/sqlite/migrations/001.do.sql | 4 + .../fixtures/sqlite/migrations/001.undo.sql | 1 + .../db/test/fixtures/sqlite/no-table.json | 15 + .../test/fixtures/sqlite/platformatic.db.json | 16 + packages/db/test/fixtures/sqlite/seed.js | 8 + packages/db/test/fixtures/undici-plugin.js | 15 + .../validate-migrations-checksums.json | 17 + .../test/fixtures/watch/platformatic.db.json | 16 + packages/db/test/healthcheck.test.js | 107 + packages/db/test/helper.js | 105 + packages/db/test/helper.test.js | 15 + .../db/test/load-and-reload-files.test.js | 495 ++ packages/db/test/metrics.test.js | 178 + packages/db/test/migrate/helper.mjs | 71 + packages/db/test/migrate/migrate.test.mjs | 39 + packages/db/test/migrate/sqlite3.test.mjs | 121 + packages/db/test/migrate/validations.test.mjs | 30 + packages/db/test/routes.test.js | 110 + packages/db/test/start-and-stop.test.js | 369 + packages/db/test/utils.test.js | 50 + packages/sql-graphql/.taprc | 1 + packages/sql-graphql/LICENSE | 201 + packages/sql-graphql/NOTICE | 13 + packages/sql-graphql/README.md | 13 + packages/sql-graphql/index.d.ts | 40 + packages/sql-graphql/index.js | 117 + packages/sql-graphql/lib/entity-to-type.js | 248 + packages/sql-graphql/lib/relationship.js | 67 + packages/sql-graphql/lib/utils.js | 77 + packages/sql-graphql/package.json | 44 + packages/sql-graphql/test/datatypes.test.js | 708 ++ packages/sql-graphql/test/federation.test.js | 312 + packages/sql-graphql/test/helper.js | 70 + packages/sql-graphql/test/hooks.test.js | 660 ++ packages/sql-graphql/test/ignore.test.js | 117 + packages/sql-graphql/test/insert.test.js | 181 + .../test/inserted_updated_at.test.js | 517 ++ packages/sql-graphql/test/nested.test.js | 862 ++ .../test/no-modification-of-entity.test.js | 113 + packages/sql-graphql/test/order_by.test.js | 397 + packages/sql-graphql/test/relations.test.js | 60 + packages/sql-graphql/test/simple.test.js | 708 ++ packages/sql-graphql/test/sqlite.test.js | 276 + .../sql-graphql/test/types/index.test-d.ts | 33 + packages/sql-graphql/test/where.test.js | 814 ++ packages/sql-json-schema-mapper/LICENSE | 201 + packages/sql-json-schema-mapper/NOTICE | 13 + packages/sql-json-schema-mapper/README.md | 13 + packages/sql-json-schema-mapper/index.js | 82 + packages/sql-json-schema-mapper/package.json | 31 + .../sql-json-schema-mapper/test/helper.js | 71 + .../test/simple.test.js | 56 + packages/sql-mapper/.taprc | 1 + packages/sql-mapper/LICENSE | 201 + packages/sql-mapper/NOTICE | 13 + packages/sql-mapper/README.md | 13 + packages/sql-mapper/lib/entity.js | 287 + packages/sql-mapper/lib/queries/index.js | 23 + packages/sql-mapper/lib/queries/mariadb.js | 11 + .../sql-mapper/lib/queries/mysql-shared.js | 62 + packages/sql-mapper/lib/queries/mysql.js | 104 + packages/sql-mapper/lib/queries/pg.js | 79 + packages/sql-mapper/lib/queries/shared.js | 100 + packages/sql-mapper/lib/queries/sqlite.js | 169 + packages/sql-mapper/lib/utils.js | 14 + packages/sql-mapper/mapper.d.ts | 308 + packages/sql-mapper/mapper.js | 155 + packages/sql-mapper/package.json | 44 + packages/sql-mapper/test/entity.test.js | 344 + packages/sql-mapper/test/helper.js | 66 + packages/sql-mapper/test/hooks.test.js | 325 + .../test/inserted_at_updated_at.test.js | 132 + packages/sql-mapper/test/mapper.test.js | 288 + .../sql-mapper/test/types/mapper.test-d.ts | 64 + packages/sql-mapper/test/where.test.js | 316 + packages/sql-openapi/.taprc | 1 + packages/sql-openapi/LICENSE | 201 + packages/sql-openapi/NOTICE | 13 + packages/sql-openapi/README.md | 13 + packages/sql-openapi/index.d.ts | 12 + packages/sql-openapi/index.js | 64 + packages/sql-openapi/lib/entity-to-routes.js | 286 + packages/sql-openapi/package.json | 46 + packages/sql-openapi/test/helper.js | 71 + packages/sql-openapi/test/hooks.test.js | 207 + packages/sql-openapi/test/ignore.test.js | 91 + packages/sql-openapi/test/order_by.test.js | 176 + packages/sql-openapi/test/simple.test.js | 444 + .../test/tap-snapshots/orderby-openapi-1.cjs | 531 ++ .../test/tap-snapshots/simple-openapi-1.cjs | 447 + .../test/tap-snapshots/simple-openapi-2.cjs | 446 + .../test/tap-snapshots/simple-openapi-3.cjs | 448 + .../test/tap-snapshots/where-openapi-1.cjs | 616 ++ .../test/tap-snapshots/where-openapi-2.cjs | 1181 +++ .../sql-openapi/test/types/index.test-d.ts | 38 + packages/sql-openapi/test/where.test.js | 464 ++ pnpm-lock.yaml | 7395 +++++++++++++++++ pnpm-workspace.yaml | 3 + renovate.json | 8 + scripts/copy-license.sh | 13 + scripts/db-proxy.mjs | 33 + scripts/gen-cli-doc.mjs | 119 + scripts/postinstall.js | 8 + scripts/sync-version.sh | 12 + 406 files changed, 53598 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/docker.yml create mode 100644 .github/workflows/issues.yml create mode 100644 .github/workflows/update-docs.yml create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 README.md create mode 100644 demo/auth/migrations/001.do.sql create mode 100644 demo/auth/migrations/001.undo.sql create mode 100644 demo/auth/migrations/002.do.sql create mode 100644 demo/auth/migrations/002.undo.sql create mode 100644 demo/auth/migrations/003.do.sql create mode 100644 demo/auth/migrations/003.undo.sql create mode 100644 demo/auth/platformatic.db.json create mode 100644 demo/auth/plugin.js create mode 100644 demo/basic/migrations/001.do.sql create mode 100644 demo/basic/migrations/001.undo.sql create mode 100644 demo/basic/migrations/002.do.sql create mode 100644 demo/basic/migrations/002.undo.sql create mode 100644 demo/basic/platformatic.db.json create mode 100644 docker-compose-apple-silicon.yml create mode 100644 docker-compose-mac.yml create mode 100644 docker-compose.yml create mode 100644 docs/contributing/contributing.md create mode 100644 docs/contributing/documentation-style-guide.md create mode 100644 docs/getting-started/architecture.md create mode 100644 docs/getting-started/movie-quotes-app-tutorial.md create mode 100644 docs/getting-started/platformatic-architecture.png create mode 100644 docs/getting-started/platformatid-db-architecture.excalidraw create mode 100644 docs/getting-started/quick-start-guide.md create mode 100644 docs/guides/add-custom-functionality/extend-graphql.md create mode 100644 docs/guides/add-custom-functionality/extend-rest.md create mode 100644 docs/guides/add-custom-functionality/introduction.md create mode 100644 docs/guides/add-custom-functionality/prerequisites.md create mode 100644 docs/guides/deployment.md create mode 100644 docs/guides/jwt-auth0.md create mode 100644 docs/guides/seed-a-database.md create mode 100644 docs/reference/cli.md create mode 100644 docs/reference/configuration.md create mode 100644 docs/reference/db-authorization/images/http.png create mode 100644 docs/reference/db-authorization/images/jwt.png create mode 100644 docs/reference/db-authorization/images/sources/http.excalidraw create mode 100644 docs/reference/db-authorization/images/sources/jwt.excalidraw create mode 100644 docs/reference/db-authorization/images/sources/webhook.excalidraw create mode 100644 docs/reference/db-authorization/images/webhook.png create mode 100644 docs/reference/db-authorization/introduction.md create mode 100644 docs/reference/db-authorization/programmatic-rules.md create mode 100644 docs/reference/migrations.md create mode 100644 docs/reference/plugin.md create mode 100644 docs/reference/sql-graphql/examples/deleteEntity.js create mode 100644 docs/reference/sql-graphql/examples/insertEntity.js create mode 100644 docs/reference/sql-graphql/examples/query.js create mode 100644 docs/reference/sql-graphql/examples/saveEntity.js create mode 100644 docs/reference/sql-graphql/introduction.md create mode 100644 docs/reference/sql-graphql/mutations.md create mode 100644 docs/reference/sql-graphql/queries.md create mode 100644 docs/reference/sql-mapper/entities/api.md create mode 100644 docs/reference/sql-mapper/entities/example.md create mode 100644 docs/reference/sql-mapper/entities/fields.md create mode 100644 docs/reference/sql-mapper/entities/hooks.md create mode 100644 docs/reference/sql-mapper/entities/introduction.md create mode 100644 docs/reference/sql-mapper/entities/relations.md create mode 100644 docs/reference/sql-mapper/examples/delete.js create mode 100644 docs/reference/sql-mapper/examples/fastify-plugin.js create mode 100644 docs/reference/sql-mapper/examples/fields.js create mode 100644 docs/reference/sql-mapper/examples/find.js create mode 100644 docs/reference/sql-mapper/examples/hooks.js create mode 100644 docs/reference/sql-mapper/examples/insert.js create mode 100644 docs/reference/sql-mapper/examples/relations.js create mode 100644 docs/reference/sql-mapper/examples/save.js create mode 100644 docs/reference/sql-mapper/fastify-plugin.md create mode 100644 docs/reference/sql-mapper/images/plt-db-hooks.svg create mode 100644 docs/reference/sql-mapper/introduction.md create mode 100644 docs/reference/sql-rest/api.md create mode 100644 docs/reference/sql-rest/introduction.md create mode 100644 package.json create mode 100644 packages/authenticate/LICENSE create mode 100644 packages/authenticate/NOTICE create mode 100644 packages/authenticate/README.md create mode 100644 packages/authenticate/authenticate.js create mode 100644 packages/authenticate/lib/login.js create mode 100644 packages/authenticate/lib/schema.js create mode 100644 packages/authenticate/lib/utils.js create mode 100644 packages/authenticate/package.json create mode 100644 packages/authenticate/test/login.test.js create mode 100644 packages/cli/.npmignore create mode 100644 packages/cli/.taprc create mode 100644 packages/cli/LICENSE create mode 100644 packages/cli/NOTICE create mode 100644 packages/cli/README.md create mode 100755 packages/cli/cli.js create mode 100644 packages/cli/help/help.txt create mode 100644 packages/cli/lib/ascii.js create mode 100644 packages/cli/package.json create mode 100644 packages/cli/test/helper.js create mode 100644 packages/cli/test/platformatic.test.js create mode 100644 packages/config/LICENSE create mode 100644 packages/config/NOTICE create mode 100644 packages/config/README.md create mode 100644 packages/config/index.d.ts create mode 100644 packages/config/index.js create mode 100644 packages/config/lib/plugin.js create mode 100644 packages/config/package.json create mode 100644 packages/config/test/fixtures/bad-placeholder.json create mode 100644 packages/config/test/fixtures/simple.json create mode 100644 packages/config/test/fixtures/simple.json5 create mode 100644 packages/config/test/fixtures/simple.toml create mode 100644 packages/config/test/fixtures/simple.yaml create mode 100644 packages/config/test/fixtures/sqlite.json create mode 100644 packages/config/test/helper.js create mode 100644 packages/config/test/index.test.js create mode 100644 packages/config/test/load.test.js create mode 100644 packages/config/test/placeholders.test.js create mode 100644 packages/config/test/plugin.test.js create mode 100644 packages/config/test/save.test.js create mode 100644 packages/config/test/watch.test.js create mode 100644 packages/db-authorization/.npmignore create mode 100644 packages/db-authorization/.taprc create mode 100644 packages/db-authorization/LICENSE create mode 100644 packages/db-authorization/NOTICE create mode 100644 packages/db-authorization/README.md create mode 100644 packages/db-authorization/index.js create mode 100644 packages/db-authorization/lib/find-rule.d.ts create mode 100644 packages/db-authorization/lib/find-rule.js create mode 100644 packages/db-authorization/lib/jwt.js create mode 100644 packages/db-authorization/lib/utils.d.ts create mode 100644 packages/db-authorization/lib/utils.js create mode 100644 packages/db-authorization/lib/webhook.js create mode 100644 packages/db-authorization/package.json create mode 100644 packages/db-authorization/schema.js create mode 100644 packages/db-authorization/test/admin.test.js create mode 100644 packages/db-authorization/test/code-permissions.test.js create mode 100644 packages/db-authorization/test/fields-permissions.test.js create mode 100644 packages/db-authorization/test/find-rule.test.js create mode 100644 packages/db-authorization/test/helper.js create mode 100644 packages/db-authorization/test/jwt.test.js create mode 100644 packages/db-authorization/test/multi-role.test.js create mode 100644 packages/db-authorization/test/nested.test.js create mode 100644 packages/db-authorization/test/set-from-code.test.js create mode 100644 packages/db-authorization/test/simple-rest.test.js create mode 100644 packages/db-authorization/test/simple.test.js create mode 100644 packages/db-authorization/test/utils.test.js create mode 100644 packages/db-authorization/test/webhook.test.js create mode 100644 packages/db-core/.npmignore create mode 100644 packages/db-core/.taprc create mode 100644 packages/db-core/LICENSE create mode 100644 packages/db-core/NOTICE create mode 100644 packages/db-core/README.md create mode 100644 packages/db-core/index.js create mode 100644 packages/db-core/package.json create mode 100644 packages/db-core/test/basic.test.js create mode 100644 packages/db-core/test/helper.js create mode 100644 packages/db-dashboard/.gitignore create mode 100644 packages/db-dashboard/LICENSE create mode 100644 packages/db-dashboard/NOTICE create mode 100644 packages/db-dashboard/README.md create mode 100644 packages/db-dashboard/index.html create mode 100644 packages/db-dashboard/index.js create mode 100644 packages/db-dashboard/package.json create mode 100644 packages/db-dashboard/playwright.config.js create mode 100644 packages/db-dashboard/public/images/apple-touch-icon.png create mode 100644 packages/db-dashboard/public/images/favicon-16x16.png create mode 100644 packages/db-dashboard/public/images/favicon-32x32.png create mode 100644 packages/db-dashboard/public/images/favicon.ico create mode 100644 packages/db-dashboard/public/images/logo-192x192.png create mode 100644 packages/db-dashboard/public/images/logo-512x512.png create mode 100644 packages/db-dashboard/public/index.html create mode 100644 packages/db-dashboard/public/manifest.json create mode 100644 packages/db-dashboard/public/robots.txt create mode 100644 packages/db-dashboard/schema.js create mode 100644 packages/db-dashboard/src/App.css create mode 100644 packages/db-dashboard/src/App.jsx create mode 100644 packages/db-dashboard/src/assets/react.svg create mode 100644 packages/db-dashboard/src/components/Layout.jsx create mode 100644 packages/db-dashboard/src/components/LoginBox.jsx create mode 100644 packages/db-dashboard/src/components/Navbar.jsx create mode 100644 packages/db-dashboard/src/components/Navbar.module.css create mode 100644 packages/db-dashboard/src/components/Navbar.test.jsx create mode 100644 packages/db-dashboard/src/components/Notification.jsx create mode 100644 packages/db-dashboard/src/components/Sidebar.css create mode 100644 packages/db-dashboard/src/components/Sidebar.jsx create mode 100644 packages/db-dashboard/src/components/Sidebar.test.jsx create mode 100644 packages/db-dashboard/src/elements/Title.jsx create mode 100644 packages/db-dashboard/src/index.css create mode 100644 packages/db-dashboard/src/main.jsx create mode 100644 packages/db-dashboard/src/pages/ConfigViewer.jsx create mode 100644 packages/db-dashboard/src/pages/GQL.jsx create mode 100644 packages/db-dashboard/src/pages/GQL.module.css create mode 100644 packages/db-dashboard/src/pages/Home.jsx create mode 100644 packages/db-dashboard/src/pages/SwaggerViewer.jsx create mode 100644 packages/db-dashboard/src/reportWebVitals.js create mode 100644 packages/db-dashboard/src/setupTests.js create mode 100644 packages/db-dashboard/src/utils.js create mode 100644 packages/db-dashboard/test/e2e/fixtures/e2e-test-config.json create mode 100644 packages/db-dashboard/test/e2e/fixtures/migrations/001.do.sql create mode 100644 packages/db-dashboard/test/e2e/fixtures/migrations/001.undo.sql create mode 100644 packages/db-dashboard/test/e2e/fixtures/migrations/002.do.sql create mode 100644 packages/db-dashboard/test/e2e/fixtures/migrations/002.undo.sql create mode 100644 packages/db-dashboard/test/e2e/index.spec.js create mode 100644 packages/db-dashboard/vite.config.js create mode 100644 packages/db-dashboard/vitest.config.js create mode 100644 packages/db/.npmignore create mode 100644 packages/db/.taprc create mode 100644 packages/db/LICENSE create mode 100644 packages/db/NOTICE create mode 100644 packages/db/README.md create mode 100644 packages/db/_admin/auth-routes.js create mode 100644 packages/db/_admin/index.js create mode 100644 packages/db/_admin/non-auth-routes.js create mode 100755 packages/db/db.mjs create mode 100644 packages/db/fixtures/bad-migrations.json create mode 100644 packages/db/fixtures/bad-migrations/001.do.sql create mode 100644 packages/db/fixtures/bad-migrations/001.undo.sql create mode 100644 packages/db/fixtures/bad-migrations/002.do.sql create mode 100644 packages/db/fixtures/migrations/001.do.sql create mode 100644 packages/db/fixtures/migrations/001.undo.sql create mode 100644 packages/db/fixtures/no-connectionString.json create mode 100644 packages/db/fixtures/no-migrations-dir.json create mode 100644 packages/db/fixtures/no-migrations.json create mode 100644 packages/db/fixtures/simple.json create mode 100644 packages/db/fixtures/sqlite/migrations/001.do.sql create mode 100644 packages/db/fixtures/sqlite/migrations/001.undo.sql create mode 100644 packages/db/fixtures/sqlite/platformatic.db.json create mode 100644 packages/db/help/help.txt create mode 100644 packages/db/help/init.txt create mode 100644 packages/db/help/migrate.txt create mode 100644 packages/db/help/schema.txt create mode 100644 packages/db/help/seed.txt create mode 100644 packages/db/help/start.txt create mode 100644 packages/db/help/types.txt create mode 100644 packages/db/index.js create mode 100644 packages/db/lib/config.js create mode 100644 packages/db/lib/errors.mjs create mode 100644 packages/db/lib/gen-schema.mjs create mode 100644 packages/db/lib/gen-types.mjs create mode 100644 packages/db/lib/helper.js create mode 100644 packages/db/lib/init.mjs create mode 100644 packages/db/lib/load-config.mjs create mode 100644 packages/db/lib/metrics-plugin.js create mode 100755 packages/db/lib/migrate.mjs create mode 100644 packages/db/lib/migrator.mjs create mode 100644 packages/db/lib/root-endpoint/index.js create mode 100644 packages/db/lib/root-endpoint/public/index.html create mode 100644 packages/db/lib/root-endpoint/public/logo-512x512.png create mode 100644 packages/db/lib/schema.js create mode 100644 packages/db/lib/seed.mjs create mode 100644 packages/db/lib/start.mjs create mode 100644 packages/db/lib/utils.js create mode 100644 packages/db/package.json create mode 100644 packages/db/tap-snapshots/test/cli/env.test.mjs.test.cjs create mode 100644 packages/db/tap-snapshots/test/cli/schema.test.mjs.test.cjs create mode 100644 packages/db/test/admin.test.js create mode 100644 packages/db/test/authorization-setup.test.js create mode 100644 packages/db/test/cli/env.test.mjs create mode 100644 packages/db/test/cli/gen-types.test.mjs create mode 100644 packages/db/test/cli/help.test.mjs create mode 100644 packages/db/test/cli/helper.mjs create mode 100644 packages/db/test/cli/init.test.mjs create mode 100644 packages/db/test/cli/load-and-reload-files.test.mjs create mode 100644 packages/db/test/cli/migrate.test.mjs create mode 100644 packages/db/test/cli/schema.test.mjs create mode 100644 packages/db/test/cli/seed.test.mjs create mode 100644 packages/db/test/cli/sqlite3.test.mjs create mode 100644 packages/db/test/cli/start.test.mjs create mode 100644 packages/db/test/cli/validations.test.mjs create mode 100644 packages/db/test/cli/watch.test.mjs create mode 100644 packages/db/test/config.test.js create mode 100644 packages/db/test/cors.test.js create mode 100644 packages/db/test/fixtures/auto-apply.json create mode 100644 packages/db/test/fixtures/auto-gen-types/index.d.ts create mode 100644 packages/db/test/fixtures/auto-gen-types/index.test-d.ts create mode 100644 packages/db/test/fixtures/auto-gen-types/migrations/001.do.sql create mode 100644 packages/db/test/fixtures/auto-gen-types/package.json create mode 100644 packages/db/test/fixtures/auto-gen-types/platformatic.db.json create mode 100644 packages/db/test/fixtures/auto/migrations/001.do.sql create mode 100644 packages/db/test/fixtures/auto/migrations/001.undo.sql create mode 100644 packages/db/test/fixtures/auto/platformatic.db.json create mode 100644 packages/db/test/fixtures/env-whitelist-default.json create mode 100644 packages/db/test/fixtures/env-whitelist.json create mode 100644 packages/db/test/fixtures/gen-types/db create mode 100644 packages/db/test/fixtures/gen-types/index.d.ts create mode 100644 packages/db/test/fixtures/gen-types/index.test-d.ts create mode 100644 packages/db/test/fixtures/gen-types/migrations/001.do.sql create mode 100644 packages/db/test/fixtures/gen-types/package.json create mode 100644 packages/db/test/fixtures/gen-types/platformatic.db.json create mode 100644 packages/db/test/fixtures/init/.gitkeep create mode 100644 packages/db/test/fixtures/init/db.sqlite create mode 100644 packages/db/test/fixtures/init/package.json create mode 100644 packages/db/test/fixtures/invalid-migrations-directory.json create mode 100644 packages/db/test/fixtures/migrations/001.do.sql create mode 100644 packages/db/test/fixtures/migrations/001.undo.sql create mode 100644 packages/db/test/fixtures/missing-required-values.json create mode 100644 packages/db/test/fixtures/no-server-logger.json create mode 100644 packages/db/test/fixtures/placeholder.json create mode 100644 packages/db/test/fixtures/root-endpoint-plugin.js create mode 100644 packages/db/test/fixtures/simple.json create mode 100644 packages/db/test/fixtures/sqlite/ignore.json create mode 100644 packages/db/test/fixtures/sqlite/migrations/001.do.sql create mode 100644 packages/db/test/fixtures/sqlite/migrations/001.undo.sql create mode 100644 packages/db/test/fixtures/sqlite/no-table.json create mode 100644 packages/db/test/fixtures/sqlite/platformatic.db.json create mode 100644 packages/db/test/fixtures/sqlite/seed.js create mode 100644 packages/db/test/fixtures/undici-plugin.js create mode 100644 packages/db/test/fixtures/validate-migrations-checksums.json create mode 100644 packages/db/test/fixtures/watch/platformatic.db.json create mode 100644 packages/db/test/healthcheck.test.js create mode 100644 packages/db/test/helper.js create mode 100644 packages/db/test/helper.test.js create mode 100644 packages/db/test/load-and-reload-files.test.js create mode 100644 packages/db/test/metrics.test.js create mode 100644 packages/db/test/migrate/helper.mjs create mode 100644 packages/db/test/migrate/migrate.test.mjs create mode 100644 packages/db/test/migrate/sqlite3.test.mjs create mode 100644 packages/db/test/migrate/validations.test.mjs create mode 100644 packages/db/test/routes.test.js create mode 100644 packages/db/test/start-and-stop.test.js create mode 100644 packages/db/test/utils.test.js create mode 100644 packages/sql-graphql/.taprc create mode 100644 packages/sql-graphql/LICENSE create mode 100644 packages/sql-graphql/NOTICE create mode 100644 packages/sql-graphql/README.md create mode 100644 packages/sql-graphql/index.d.ts create mode 100644 packages/sql-graphql/index.js create mode 100644 packages/sql-graphql/lib/entity-to-type.js create mode 100644 packages/sql-graphql/lib/relationship.js create mode 100644 packages/sql-graphql/lib/utils.js create mode 100644 packages/sql-graphql/package.json create mode 100644 packages/sql-graphql/test/datatypes.test.js create mode 100644 packages/sql-graphql/test/federation.test.js create mode 100644 packages/sql-graphql/test/helper.js create mode 100644 packages/sql-graphql/test/hooks.test.js create mode 100644 packages/sql-graphql/test/ignore.test.js create mode 100644 packages/sql-graphql/test/insert.test.js create mode 100644 packages/sql-graphql/test/inserted_updated_at.test.js create mode 100644 packages/sql-graphql/test/nested.test.js create mode 100644 packages/sql-graphql/test/no-modification-of-entity.test.js create mode 100644 packages/sql-graphql/test/order_by.test.js create mode 100644 packages/sql-graphql/test/relations.test.js create mode 100644 packages/sql-graphql/test/simple.test.js create mode 100644 packages/sql-graphql/test/sqlite.test.js create mode 100644 packages/sql-graphql/test/types/index.test-d.ts create mode 100644 packages/sql-graphql/test/where.test.js create mode 100644 packages/sql-json-schema-mapper/LICENSE create mode 100644 packages/sql-json-schema-mapper/NOTICE create mode 100644 packages/sql-json-schema-mapper/README.md create mode 100644 packages/sql-json-schema-mapper/index.js create mode 100644 packages/sql-json-schema-mapper/package.json create mode 100644 packages/sql-json-schema-mapper/test/helper.js create mode 100644 packages/sql-json-schema-mapper/test/simple.test.js create mode 100644 packages/sql-mapper/.taprc create mode 100644 packages/sql-mapper/LICENSE create mode 100644 packages/sql-mapper/NOTICE create mode 100644 packages/sql-mapper/README.md create mode 100644 packages/sql-mapper/lib/entity.js create mode 100644 packages/sql-mapper/lib/queries/index.js create mode 100644 packages/sql-mapper/lib/queries/mariadb.js create mode 100644 packages/sql-mapper/lib/queries/mysql-shared.js create mode 100644 packages/sql-mapper/lib/queries/mysql.js create mode 100644 packages/sql-mapper/lib/queries/pg.js create mode 100644 packages/sql-mapper/lib/queries/shared.js create mode 100644 packages/sql-mapper/lib/queries/sqlite.js create mode 100644 packages/sql-mapper/lib/utils.js create mode 100644 packages/sql-mapper/mapper.d.ts create mode 100644 packages/sql-mapper/mapper.js create mode 100644 packages/sql-mapper/package.json create mode 100644 packages/sql-mapper/test/entity.test.js create mode 100644 packages/sql-mapper/test/helper.js create mode 100644 packages/sql-mapper/test/hooks.test.js create mode 100644 packages/sql-mapper/test/inserted_at_updated_at.test.js create mode 100644 packages/sql-mapper/test/mapper.test.js create mode 100644 packages/sql-mapper/test/types/mapper.test-d.ts create mode 100644 packages/sql-mapper/test/where.test.js create mode 100644 packages/sql-openapi/.taprc create mode 100644 packages/sql-openapi/LICENSE create mode 100644 packages/sql-openapi/NOTICE create mode 100644 packages/sql-openapi/README.md create mode 100644 packages/sql-openapi/index.d.ts create mode 100644 packages/sql-openapi/index.js create mode 100644 packages/sql-openapi/lib/entity-to-routes.js create mode 100644 packages/sql-openapi/package.json create mode 100644 packages/sql-openapi/test/helper.js create mode 100644 packages/sql-openapi/test/hooks.test.js create mode 100644 packages/sql-openapi/test/ignore.test.js create mode 100644 packages/sql-openapi/test/order_by.test.js create mode 100644 packages/sql-openapi/test/simple.test.js create mode 100644 packages/sql-openapi/test/tap-snapshots/orderby-openapi-1.cjs create mode 100644 packages/sql-openapi/test/tap-snapshots/simple-openapi-1.cjs create mode 100644 packages/sql-openapi/test/tap-snapshots/simple-openapi-2.cjs create mode 100644 packages/sql-openapi/test/tap-snapshots/simple-openapi-3.cjs create mode 100644 packages/sql-openapi/test/tap-snapshots/where-openapi-1.cjs create mode 100644 packages/sql-openapi/test/tap-snapshots/where-openapi-2.cjs create mode 100644 packages/sql-openapi/test/types/index.test-d.ts create mode 100644 packages/sql-openapi/test/where.test.js create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 renovate.json create mode 100644 scripts/copy-license.sh create mode 100644 scripts/db-proxy.mjs create mode 100644 scripts/gen-cli-doc.mjs create mode 100644 scripts/postinstall.js create mode 100755 scripts/sync-version.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..0c880bd262 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,112 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env* + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +packages/db/fixtures/sqlite/db +packages/db/test/fixtures/sqlite/db + +# packages/dashboard specific rules +packages/db-dashboard/build/ +playwright-report +.DS_Store +.swp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..1bfe7e4da5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,227 @@ +name: Run tests + +on: + push: + branches: + - main + paths-ignore: + - 'docs/**' + - '**.md' + pull_request: + paths-ignore: + - 'docs/**' + - '**.md' + +# This allows a subsequently queued workflow run to interrupt previous runs +concurrency: + group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" + cancel-in-progress: true + +env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + +jobs: + setup-node_modules: + runs-on: ${{matrix.os}} + timeout-minutes: 15 + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2.2.2 + - uses: actions/setup-node@v3 + with: + node-version: 18 + cache: 'pnpm' + - name: pnpm fetch + run: pnpm fetch + + ci-cli: + needs: setup-node_modules + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + strategy: + matrix: + node-version: [16, 18] + os: [ubuntu-latest, windows-latest] + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2.2.2 + - uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + - name: pnpm install + run: pnpm install + - name: Run test suite + run: cd packages/cli && pnpm test + + ci-db-dashboard: + needs: setup-node_modules + runs-on: ${{matrix.os}} + timeout-minutes: 5 + strategy: + matrix: + node-version: [16, 18] + os: [ubuntu-latest] + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2.2.2 + - uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + - name: pnpm install + run: pnpm install + - name: Builds the dashboard + run: npm run dashboard:build + - name: Run test suite Dashboard + run: cd packages/db-dashboard && pnpm test + + ci-config: + needs: setup-node_modules + runs-on: ${{matrix.os}} + timeout-minutes: 15 + strategy: + matrix: + node-version: [16, 18] + os: [ubuntu-latest, windows-latest] + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2.2.2 + - uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + - name: pnpm install + run: pnpm install --offline + - name: Run test suite config manager + run: cd packages/config && pnpm test + + ci-db: + needs: setup-node_modules + runs-on: ${{matrix.os}} + timeout-minutes: 15 + strategy: + matrix: + node-version: [16, 18] + os: [ubuntu-latest, windows-latest] + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2.2.2 + - uses: ikalnytskyi/action-setup-postgres@v3 + - uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + - name: pnpm install + run: pnpm install --offline + - name: Builds the dashboard + run: pnpm run dashboard:build + - name: Run test suite core + run: cd packages/db-core && pnpm test + - name: Run test suite Platformatic DB + run: cd packages/db && pnpm test + + ci-db-authorization: + needs: setup-node_modules + runs-on: ${{matrix.os}} + timeout-minutes: 5 + strategy: + matrix: + node-version: [16, 18] + os: [ubuntu-latest] + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2.2.2 + - uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + - name: Start docker containers for testing + run: docker-compose up -d postgresql + - name: pnpm install + run: pnpm install --offline + - name: Run test suite + run: cd packages/db-authorization && pnpm test + + ci-db-core: + needs: setup-node_modules + runs-on: ${{matrix.os}} + timeout-minutes: 5 + strategy: + matrix: + db: [postgresql, mariadb, mysql, mysql8, sqlite] + node-version: [16, 18] + os: [ubuntu-latest] + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2.2.2 + - uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + - name: Start docker containers for testing + run: docker-compose up -d ${{ matrix.db }} + if: ${{ matrix.db != 'sqlite' }} + - name: pnpm install + run: pnpm install --offline + - name: Wait for DB + run: sleep 10 + if: ${{ matrix.db != 'sqlite' }} + - name: Run test suite sql-mapper + run: cd packages/sql-mapper && pnpm run test:typescript && pnpm run test:${{ matrix.db }}; cd ../.. + - name: Run test suite sql-json-schema-mapper + run: cd packages/sql-json-schema-mapper && pnpm run test:${{ matrix.db }}; cd ../.. + - name: Run test suite sql-openapi + run: cd packages/sql-openapi && pnpm run test:typescript && pnpm run test:${{ matrix.db }}; cd ../.. + - name: Run test suite sql-graphql + run: cd packages/sql-graphql && pnpm run test:typescript && pnpm run test:${{ matrix.db }}; cd .. + + ci-auth-login: + needs: setup-node_modules + runs-on: ${{ matrix.os }} + timeout-minutes: 5 + strategy: + matrix: + node-version: [16, 18] + os: [ubuntu-latest, windows-latest] + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2.2.2 + - uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + - name: pnpm install + run: pnpm install --offline + - name: Run test suite + run: cd packages/authenticate && pnpm test; cd ../.. + + playwright-e2e: + needs: setup-node_modules + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2.2.2 + - uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + - name: Start docker containers for testing + run: docker-compose up -d postgresql + - name: pnpm install + run: pnpm install --offline --frozen-lockfile + - name: Builds the dashboard + run: pnpm run dashboard:build + - name: Install Playwright browsers + run: cd packages/db-dashboard && pnpm exec playwright install + - name: Wait for DB + run: sleep 10 + - name: Run Platformatic DB server and E2E tests + run: | + node ./packages/cli/cli.js db --config=./packages/db-dashboard/test/e2e/fixtures/e2e-test-config.json & + sleep 5 && + cd packages/db-dashboard && pnpm run test:e2e diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000000..b87605de55 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,29 @@ +name: docker build + +on: + push: + branches: + - main + +jobs: + buildx: + runs-on: ubuntu-latest + environment: main + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v3 + with: + push: true + tags: platformatic/platformatic-private:latest + platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml new file mode 100644 index 0000000000..40bab1d870 --- /dev/null +++ b/.github/workflows/issues.yml @@ -0,0 +1,25 @@ +name: Add new issue/PR to project + +on: + issues: + types: + - opened + +jobs: + add-to-project: + name: Add issue or PR to project + runs-on: ubuntu-latest + steps: + - name: Generate token + id: generate_token + uses: vidavidorra/github-app-token@v1.0.0 + with: + appId: ${{ secrets.INTERNAL_GH_APP_ID }} + privateKey: ${{ secrets.INTERNAL_GH_APP_SECRET }} + - name: Add to Project + env: + TOKEN: ${{ steps.generate_token.outputs.token }} + uses: actions/add-to-project@338ac1805ece459f9c25a3e7a2b749fec994576d + with: + project-url: https://github.com/orgs/platformatic/projects/1 + github-token: ${{ env.TOKEN }} diff --git a/.github/workflows/update-docs.yml b/.github/workflows/update-docs.yml new file mode 100644 index 0000000000..a0960e65a6 --- /dev/null +++ b/.github/workflows/update-docs.yml @@ -0,0 +1,23 @@ +name: "Trigger OSS repo" +on: + release: + types: [published] + + push: + branches: + - main + paths: + - 'docs/**' + - '**.md' +jobs: + build-and-publish: + runs-on: ubuntu-latest + steps: + - name: Update docs + if: ${{ github.event_name == 'release' }} + run: | + curl -XPOST -u "${{ secrets.GH_API_USERNAME }}:${{ secrets.GH_API_TOKEN }}" -H "Accept: application/vnd.github.everest-preview+json" -H "Content-Type: application/json" https://api.github.com/repos/platformatic/oss/dispatches --data '{"event_type": "update_docs"}' + - name: Force update docs + if: ${{ github.event_name == 'push' }} + run: | + curl -XPOST -u "${{ secrets.GH_API_USERNAME }}:${{ secrets.GH_API_TOKEN }}" -H "Accept: application/vnd.github.everest-preview+json" -H "Content-Type: application/json" https://api.github.com/repos/platformatic/oss/dispatches --data '{"event_type": "update_docs", "inputs": { "force": true }}' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..0c880bd262 --- /dev/null +++ b/.gitignore @@ -0,0 +1,112 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env* + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +packages/db/fixtures/sqlite/db +packages/db/test/fixtures/sqlite/db + +# packages/dashboard specific rules +packages/db-dashboard/build/ +playwright-report +.DS_Store +.swp diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000..9459312b53 --- /dev/null +++ b/.npmrc @@ -0,0 +1,3 @@ +package-lock=true +auto-install-peers=true +strict-peer-dependencies=false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..19e1bf63d6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,94 @@ +# Platformatic + +## Running and Developing DB + +### Preparation + +1. Clone this repository +1. Install pnpm `npm i pnpm --location=global` +2. Install dependencies for root project: `pnpm i` +4. Install docker with Docker Desktop or [Colima](https://github.com/abiosoft/colima) + + +### Start the RDBMS + +We use Docker to start all the databases we develop against. + +On Linux, execute: `docker compose up` + +On Intel Macs: `docker compose up -f docker-compose-mac.yml` + +On Apple Silicon Macs: `docker compose up -f docker-compose-apple-silicon.yml` + +### Start platformatic db + +Create directories to work from: + +```sh +mkdir -p my-demo/migrations +``` + +Install all dependencies: +```sh +pnpm i +``` + +The CLI package is now available at **./node_modules/.bin/platformatic**. Use +`pnpm link` to use `platformatic` everywhere. +```sh +(cd packages/cli && pnpm link) +``` + +### Run dashboard development server + +Use the command +```sh +npm run dashboard:start +``` + +This will start a webpack server on port `3000` by default, with watcher and hot-reload (as a standard `create-react-app` application). + +Note that GraphiQL will _not_ work because platformatic-db has not been started +yet. + +### Run platformatic-db service + +First build the dashboard for production with the command +```sh +pnpm run dashboard:build +``` + +This will create compressed files and assets under **packages/dashboard/build** directory. +To run the service: +```sh +platformatic db +``` +This will load config from local directory (i.e using config file **platformatic.db.json**). + +If you want to use another config file use the option `--config=/path/to/some.json`. + +### Testing + +1. [Run docker](#run-docker) +1. Run `npm run dashboard:build` +1. Run tests: `npm test` + +### Releasing + +All platformatic modules share the same release number and are released +in a single process. In order to avoid internal breakages, dependencies as +part of this repository are using the `workspace:*` which will be replaced +by precise versions during publish by pnpm. + +The procedure to release is simple: + +1. Update the version of the root `package.json` +1. run `./scripts/sync-version.sh` +1. run `pnpm -r publish` + +### Creating and merging a PR +On the top of the PR description, if this is a fix of a github issue, add: +``` +fixes #issuenum +``` +When all checks are passed and the changes are approved, merge the PR with `squash and merge` option diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..ada0bb99da --- /dev/null +++ b/Dockerfile @@ -0,0 +1,45 @@ +FROM node:18-alpine + +ENV HOME=/home +ENV PLT_HOME=$HOME/platformatic/ +ENV PNPM_HOME=$HOME/pnpm +ENV APP_HOME=$HOME/app +ENV PATH=/home/pnpm:$PATH + +RUN mkdir $PNPM_HOME + +# Install Platformatic in the $PLT_HOME folder +WORKDIR $PLT_HOME + +# Install required packages +RUN apk update && apk add --no-cache dumb-init python3 libc-dev make g++ + +# Install pnpm +RUN npm i pnpm --location=global + +# Copy lock files +COPY package.json ./ +COPY pnpm-lock.yaml ./ +COPY pnpm-workspace.yaml ./ + +# Fetch all dependencies +RUN pnpm fetch --prod + +# Copy files +COPY . . + +# Install all the deps in the source code +RUN pnpm install --frozen-lockfile --prod --offline + +# Add platformatic to path +RUN cd packages/cli && pnpm link --global + +# Move to the app directory +WORKDIR $APP_HOME + +# Reduce our permissions from root to a normal user +RUN chown node:node . +USER node + +ENTRYPOINT ["dumb-init"] +CMD ["platformatic"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000000..a7d8a8414a --- /dev/null +++ b/NOTICE @@ -0,0 +1,13 @@ + Copyright 2022 Platformatic + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000000..a1bed5dc36 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# Platformatic + +Platformatic is a set a Open Source tools that you can use to build your own +_Internal Developer Platform_. + +The first of these tools is **Platformatic DB** — more will follow! + +## Install + +```bash +npm install platformatic + +# Start a new project +npx platformatic db init +``` + +Follow our [Quick Start Guide](https://oss.platformatic.dev/docs/getting-started/quick-start-guide) +guide to get up and running with Platformatic DB. + +## Documentation + +- [Getting Started](https://oss.platformatic.dev/docs/category/getting-started) +- [Reference](https://oss.platformatic.dev/docs/category/reference) +- [Guides](https://oss.platformatic.dev/docs/category/guides) + +Check out our full documentation at [oss.platformatic.dev](https://oss.platformatic.dev). + +## Support + +Having issues? Drop in to the [Platformatic Discord](https://discord.com/channels/1011258196905689118/1011258204371554307) +for help. + +## License + +Apache 2.0 diff --git a/demo/auth/migrations/001.do.sql b/demo/auth/migrations/001.do.sql new file mode 100644 index 0000000000..0a09b9f9cb --- /dev/null +++ b/demo/auth/migrations/001.do.sql @@ -0,0 +1,4 @@ +CREATE TABLE pages ( + id SERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL +); diff --git a/demo/auth/migrations/001.undo.sql b/demo/auth/migrations/001.undo.sql new file mode 100644 index 0000000000..f5465cf307 --- /dev/null +++ b/demo/auth/migrations/001.undo.sql @@ -0,0 +1 @@ +DROP TABLE pages; diff --git a/demo/auth/migrations/002.do.sql b/demo/auth/migrations/002.do.sql new file mode 100644 index 0000000000..098ff52de4 --- /dev/null +++ b/demo/auth/migrations/002.do.sql @@ -0,0 +1,5 @@ +CREATE TABLE categories ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL +); +ALTER TABLE pages ADD COLUMN category_id INTEGER REFERENCES categories(id); diff --git a/demo/auth/migrations/002.undo.sql b/demo/auth/migrations/002.undo.sql new file mode 100644 index 0000000000..048007a86d --- /dev/null +++ b/demo/auth/migrations/002.undo.sql @@ -0,0 +1,2 @@ +ALTER TABLE pages DROP COLUMN category_id; +DROP TABLE categories; diff --git a/demo/auth/migrations/003.do.sql b/demo/auth/migrations/003.do.sql new file mode 100644 index 0000000000..08ce54b5a2 --- /dev/null +++ b/demo/auth/migrations/003.do.sql @@ -0,0 +1 @@ +ALTER TABLE pages ADD COLUMN user_id INTEGER; diff --git a/demo/auth/migrations/003.undo.sql b/demo/auth/migrations/003.undo.sql new file mode 100644 index 0000000000..9fcc1cee49 --- /dev/null +++ b/demo/auth/migrations/003.undo.sql @@ -0,0 +1 @@ +ALTER TABLE pages DROP COLUMN user_id; diff --git a/demo/auth/platformatic.db.json b/demo/auth/platformatic.db.json new file mode 100644 index 0000000000..7ba2c3b6f9 --- /dev/null +++ b/demo/auth/platformatic.db.json @@ -0,0 +1,51 @@ +{ + "server": { + "logger": { + "level": "info" + }, + "hostname": "127.0.0.1", + "port": "3042" + }, + "core": { + "connectionString": "postgres://postgres:postgres@127.0.0.1:5432/postgres", + "graphql": { + "graphiql": true + } + }, + "migrations": { + "dir": "./migrations" + }, + "plugin": { + "path": "./plugin.js" + }, + "authorization": { + "adminSecret": "platformatic", + "rules": [ + { + "role": "user", + "entity": "page", + "delete": false, + "defaults": { + "userId": "X-PLATFORMATIC-USER-ID" + }, + "find": { + "checks": { + "userId": "X-PLATFORMATIC-USER-ID" + } + }, + "save": { + "checks": { + "userId": "X-PLATFORMATIC-USER-ID" + } + } + }, + { + "role": "anonymous", + "entity": "page", + "find": false, + "delete": false, + "save": false + } + ] + } +} diff --git a/demo/auth/plugin.js b/demo/auth/plugin.js new file mode 100644 index 0000000000..7f681f3a06 --- /dev/null +++ b/demo/auth/plugin.js @@ -0,0 +1,34 @@ +'use strict' + +module.exports = async function app (app) { + app.log.info('loaded') + + app.get('/hello', async function () { + return { + message: 'Hello World!' + } + }) + + // console.log(await app.platformatic.entities.page.find({ fields: ['title'] })) + + app.graphql.extendSchema(` + extend type Query { + hello: String, + titles: [String] + } + `) + app.graphql.defineResolvers({ + Query: { + hello: () => 'Hello World!', + titles: async () => { + const { db, sql } = app.platformatic + + const titles = await db.query(sql` + SELECT title FROM pages + `) + + return titles.map(({ title }) => title) + } + } + }) +} diff --git a/demo/basic/migrations/001.do.sql b/demo/basic/migrations/001.do.sql new file mode 100644 index 0000000000..0a09b9f9cb --- /dev/null +++ b/demo/basic/migrations/001.do.sql @@ -0,0 +1,4 @@ +CREATE TABLE pages ( + id SERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL +); diff --git a/demo/basic/migrations/001.undo.sql b/demo/basic/migrations/001.undo.sql new file mode 100644 index 0000000000..f5465cf307 --- /dev/null +++ b/demo/basic/migrations/001.undo.sql @@ -0,0 +1 @@ +DROP TABLE pages; diff --git a/demo/basic/migrations/002.do.sql b/demo/basic/migrations/002.do.sql new file mode 100644 index 0000000000..098ff52de4 --- /dev/null +++ b/demo/basic/migrations/002.do.sql @@ -0,0 +1,5 @@ +CREATE TABLE categories ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL +); +ALTER TABLE pages ADD COLUMN category_id INTEGER REFERENCES categories(id); diff --git a/demo/basic/migrations/002.undo.sql b/demo/basic/migrations/002.undo.sql new file mode 100644 index 0000000000..048007a86d --- /dev/null +++ b/demo/basic/migrations/002.undo.sql @@ -0,0 +1,2 @@ +ALTER TABLE pages DROP COLUMN category_id; +DROP TABLE categories; diff --git a/demo/basic/platformatic.db.json b/demo/basic/platformatic.db.json new file mode 100644 index 0000000000..663ed5382e --- /dev/null +++ b/demo/basic/platformatic.db.json @@ -0,0 +1,18 @@ +{ + "server": { + "logger": { + "level": "info" + }, + "hostname": "127.0.0.1", + "port": "3042" + }, + "core": { + "connectionString": "postgres://postgres:postgres@127.0.0.1:5432/postgres", + "graphql": { + "graphiql": true + } + }, + "migrations": { + "dir": "./migrations" + } +} diff --git a/docker-compose-apple-silicon.yml b/docker-compose-apple-silicon.yml new file mode 100644 index 0000000000..ca869e6c51 --- /dev/null +++ b/docker-compose-apple-silicon.yml @@ -0,0 +1,33 @@ +version: "3.3" +services: + postgresql: + ports: + - "5432:5432" + image: "arm64v8/postgres:14-alpine" + environment: + - POSTGRES_PASSWORD=postgres + mariadb: + ports: + - "3307:3306" + image: "arm64v8/mariadb:10.9" + environment: + - MYSQL_ALLOW_EMPTY_PASSWORD=yes + - MYSQL_DATABASE=graph + mysql: + platform: 'linux/amd64' + ports: + - "3306:3306" + image: "mysql:5.7" + environment: + - MYSQL_ALLOW_EMPTY_PASSWORD=yes + - MYSQL_DATABASE=graph + mysql8: + ports: + - "3308:3306" + image: "arm64v8/mysql:8-oracle" + environment: + - MYSQL_ALLOW_EMPTY_PASSWORD=yes + - MYSQL_DATABASE=graph + + + diff --git a/docker-compose-mac.yml b/docker-compose-mac.yml new file mode 100644 index 0000000000..f542d63bd4 --- /dev/null +++ b/docker-compose-mac.yml @@ -0,0 +1,32 @@ +version: "3.3" +services: + postgresql: + ports: + - "5432:5432" + image: "postgres:14-alpine" + environment: + - POSTGRES_PASSWORD=postgres + mariadb: + ports: + - "3307:3306" + image: "mariadb:10.9" + environment: + - MYSQL_ALLOW_EMPTY_PASSWORD=yes + - MYSQL_DATABASE=graph + mysql: + ports: + - "3306:3306" + image: "mysql:5.7" + environment: + - MYSQL_ALLOW_EMPTY_PASSWORD=yes + - MYSQL_DATABASE=graph + mysql8: + ports: + - "3308:3306" + image: "mysql:8" + environment: + - MYSQL_ALLOW_EMPTY_PASSWORD=yes + - MYSQL_DATABASE=graph + + + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..94ae3cdf3e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +version: "3.3" +services: + postgresql: + ports: + - "127.0.0.1:5432:5432" + image: "postgres:14-alpine" + environment: + - POSTGRES_PASSWORD=postgres + mariadb: + ports: + - "127.0.0.1:3307:3306" + image: "mariadb:10.9" + environment: + - MYSQL_ALLOW_EMPTY_PASSWORD=yes + - MYSQL_DATABASE=graph + mysql: + ports: + - "127.0.0.1:3306:3306" + image: "mysql:5.7" + environment: + - MYSQL_ALLOW_EMPTY_PASSWORD=yes + - MYSQL_DATABASE=graph + mysql8: + ports: + - "127.0.0.1:3308:3306" + image: "mysql:8" + environment: + - MYSQL_ALLOW_EMPTY_PASSWORD=yes + - MYSQL_DATABASE=graph + + + diff --git a/docs/contributing/contributing.md b/docs/contributing/contributing.md new file mode 100644 index 0000000000..b53f66837e --- /dev/null +++ b/docs/contributing/contributing.md @@ -0,0 +1,3 @@ +# Contributing + +Details coming soon. diff --git a/docs/contributing/documentation-style-guide.md b/docs/contributing/documentation-style-guide.md new file mode 100644 index 0000000000..fbb6c09891 --- /dev/null +++ b/docs/contributing/documentation-style-guide.md @@ -0,0 +1,238 @@ +--- +credits: https://github.com/fastify/fastify/blob/main/docs/Guides/Style-Guide.md +--- + +# Documentation Style Guide + +Welcome to the *Platformatic Documentation Style Guide*. This guide is here to provide +you with a conventional writing style for users writing developer documentation on +our Open Source framework. Each topic is precise and well explained to help you write +documentation users can easily understand and implement. + +## Who is this guide for? + +This guide is for anyone who loves to build with Platformatic or wants to contribute +to our documentation. You do not need to be an expert in writing technical +documentation. This guide is here to help you. + +Visit [CONTRIBUTING.md](https://github.com/platformatic/platformatic/blob/main/CONTRIBUTING.md) +file on GitHub to join our Open Source folks. + +## Before you write + +You should have a basic understanding of: + +* JavaScript +* Node.js +* Git +* GitHub +* Markdown +* HTTP +* NPM + +### Consider your Audience + +Before you start writing, think about your audience. In this case, your audience +should already know HTTP, JavaScript, NPM, and Node.js. It is necessary to keep +your readers in mind because they are the ones consuming your content. You want +to give as much useful information as possible. Consider the vital things they +need to know and how they can understand them. Use words and references that +readers can relate to easily. Ask for feedback from the community, it can help +you write better documentation that focuses on the user and what you want to +achieve. + +### Get straight to the point + +Give your readers a clear and precise action to take. Start with what is most +important. This way, you can help them find what they need faster. Mostly, +readers tend to read the first content on a page, and many will not scroll +further. + +**Example** + +Less like this: Colons are very important to register a parametric path. It lets +the framework know there is a new parameter created. You can place the colon +before the parameter name so the parametric path can be created. + +More Like this: To register a parametric path, put a colon before the parameter +name. Using a colon lets the framework know it is a parametric path and not a +static path. + +### Images and video should enhance the written documentation + + +Images and video should only be added if they complement the written +documentation, for example to help the reader form a clearer mental model of a +concept or pattern. + +Images can be directly embedded, but videos should be included by linking to an +external site, such as YouTube. You can add links by using +`[Title](https://www.websitename.com)` in the Markdown. + + + + +### Avoid plagiarism + +Make sure you avoid copying other people's work. Keep it as original as +possible. You can learn from what they have done and reference where it is from +if you used a particular quote from their work. + + +## Word Choice + +There are a few things you need to use and avoid when writing your documentation +to improve readability for readers and make documentation neat, direct, and +clean. + + +### When to use the second person "you" as the pronoun + +When writing articles or guides, your content should communicate directly to +readers in the second person ("you") addressed form. It is easier to give them +direct instruction on what to do on a particular topic. To see an example, visit +the [Quick Start Guide](../getting-started/quick-start-guide.md). + +**Example** + +Less like this: we can use the following plugins. + +More like this: You can use the following plugins. + +> According to [Wikipedia](#), ***You*** is usually a second person pronoun. +> Also, used to refer to an indeterminate person, as a more common alternative +> to a very formal indefinite pronoun. + +## When to avoid the second person "you" as the pronoun + +One of the main rules of formal writing such as reference documentation, or API +documentation, is to avoid the second person ("you") or directly addressing the +reader. + +**Example** + +Less like this: You can use the following recommendation as an example. + +More like this: As an example, the following recommendations should be +referenced. + +To view a live example, refer to the [Decorators](../reference/configuration.md) +reference document. + + +### Avoid using contractions + +Contractions are the shortened version of written and spoken forms of a word, +i.e. using "don't" instead of "do not". Avoid contractions to provide a more +formal tone. + +### Avoid using condescending terms + +Condescending terms are words that include: + +* Just +* Easy +* Simply +* Basically +* Obviously + +The reader may not find it easy to use Platformatic; avoid +words that make it sound simple, easy, offensive, or insensitive. Not everyone +who reads the documentation has the same level of understanding. + +### Starting with a verb + +Mostly start your description with a verb, which makes it simple and precise for +the reader to follow. Prefer using present tense because it is easier to read +and understand than the past or future tense. + +**Example** + + Less like this: There is a need for Node.js to be installed before you can be + able to use Platformatic. + + More like this: Install Node.js to make use of Platformatic. + +### Grammatical moods + +Grammatical moods are a great way to express your writing. Avoid sounding too +bossy while making a direct statement. Know when to switch between indicative, +imperative, and subjunctive moods. + + +**Indicative** - Use when making a factual statement or question. + +Example: Since there is no testing framework available, "Platformatic recommends ways +to write tests". + +**Imperative** - Use when giving instructions, actions, commands, or when you +write your headings. + +Example: Install dependencies before starting development. + + +**Subjunctive** - Use when making suggestions, hypotheses, or non-factual +statements. + +Example: Reading the documentation on our website is recommended to get +comprehensive knowledge of the framework. + +### Use **active** voice instead of **passive** + +Using active voice is a more compact and direct way of conveying your +documentation. + +**Example** + + +Passive: The node dependencies and packages are installed by npm. + +Active: npm installs packages and node dependencies. + +## Writing Style + +### Documentation titles + +When creating a new guide, API, or reference in the `/docs/` directory, use +short titles that best describe the topic of your documentation. Name your files +in kebab-cases and avoid Raw or camelCase. To learn more about kebab-case you +can visit this medium article on [Case +Styles](https://medium.com/better-programming/string-case-styles-camel-pascal-snake-and-kebab-case-981407998841). + +**Examples**: + +>`hook-and-plugins.md`, + + `adding-test-plugins.md`, + + `removing-requests.md`. + +### Hyperlinks + +Hyperlinks should have a clear title of what it references. Here is how your +hyperlink should look: + +```MD + + +// Add clear & brief description +[Fastify Plugins] (https://www.fastify.io/docs/latest/Plugins/) + + + +// incomplete description +[Fastify] (https://www.fastify.io/docs/latest/Plugins/) + +// Adding title in link brackets +[](https://www.fastify.io/docs/latest/Plugins/ "fastify plugin") + +// Empty title +[](https://www.fastify.io/docs/latest/Plugins/) + +// Adding links localhost URLs instead of using code strings (``) +[http://localhost:3000/](http://localhost:3000/) + +``` + +Include in your documentation as many essential references as possible, but +avoid having numerous links when writing for beginners to avoid distractions. diff --git a/docs/getting-started/architecture.md b/docs/getting-started/architecture.md new file mode 100644 index 0000000000..6974c22547 --- /dev/null +++ b/docs/getting-started/architecture.md @@ -0,0 +1,28 @@ +# Architecture + +Platformatic is a collection of Open Source tools designed to eliminate friction +in backend development. The first of those tools is Platformatic DB, which is developed +as `@platformatic/db`. + +## Platformatic DB + +Platformatic DB can expose a SQL database by dynamically mapping it to REST/OpenAPI +and GraphQL endpoints. It supports a limited subset of the SQL query language, but +also allows developers to add their own custom routes and resolvers. + +![Platformatic DB Architecture](./platformatic-architecture.png) + +Platformatic DB is composed of a few key libraries: + +1. `@platformatic/sql-mapper` - follows the [Data Mapper pattern](https://en.wikipedia.org/wiki/Data_mapper_pattern) to build an API on top of a SQL database. + Internally it uses the [`@database` project](https://www.atdatabases.org/). +1. `@platformatic/sql-openapi` - uses `sql-mapper` to create a series of REST routes and matching OpenAPI definitions. + Internally it uses [`@fastify/swagger`](https://github.com/fastify/fastify-swagger). +1. `@platformatic/sql-graphql` - uses `sql-mapper` to create a GraphQL endpoint and schema. `sql-graphql` also support Federation. + Internally it uses [`mercurius`](https://github.com/mercuriusjs/mercurius). + +Platformatic DB allows you to load a [Fastify plugin](https://www.fastify.io/docs/latest/Reference/Plugins/) during server startup that contains your own application-specific code. +The plugin can add more routes or resolvers — these will automatically be shown in the OpenAPI and GraphQL schemas. + +SQL database migrations are also supported. They're implemented internally with the [`postgrator`](https://www.npmjs.com/package/postgrator) library. + diff --git a/docs/getting-started/movie-quotes-app-tutorial.md b/docs/getting-started/movie-quotes-app-tutorial.md new file mode 100644 index 0000000000..04cb64c874 --- /dev/null +++ b/docs/getting-started/movie-quotes-app-tutorial.md @@ -0,0 +1,1888 @@ +# Movie Quotes App Tutorial + +This tutorial will help you learn how to build a full stack application on top +of Platformatic DB. We're going to build an application that allows us to +save our favourite movie quotes. We'll also be building in custom API functionality +that allows for some neat user interaction on our frontend. + +You can find the complete code for the application that we're going to build +[on GitHub](https://github.com/platformatic/tutorial-movie-quotes-app). + +:::note + +We'll be building the frontend of our application with the [Astro](https://astro.build/) +framework, but the GraphQL API integration steps that we're going to cover can +be applied with most frontend frameworks. + +::: + +## What we're going to cover + +In this tutorial we'll learn how to: + +- Create a Platformatic API +- Apply database migrations +- Create relationships between our API entities +- Populate our database tables +- Build a frontend application that integrates with our GraphQL API +- Extend our API with custom functionality +- Enable CORS on our Platformatic API + +## Prerequisites + +To follow along with this tutorial you'll need to have these things installed: + +- [Node.js](https://nodejs.org/) >= v16.17.0 or >= v18.8.0 +- [npm](https://docs.npmjs.com/cli/) v7 or later +- A code editor, for example [Visual Studio Code](https://code.visualstudio.com/) + +You'll also need to have some experience with JavaScript, and be comfortable with +running commands in a terminal. + +## Build the backend + +### Create a Platformatic API + +First, let's create our project directory: + +```bash +mkdir -p tutorial-movie-quotes-app/apps/movie-quotes-api/ + +cd tutorial-movie-quotes-app/apps/movie-quotes-api/ +``` + +Then let's create a `package.json` file: + +```bash +npm init --yes +``` + +Now we can install the [platformatic](https://www.npmjs.com/package/platformatic) +CLI as a dependency: + +```bash +npm install platformatic +``` + +Let's also add some npm run scripts for convenience: + +```bash +npm pkg set scripts.start="platformatic db start" + +npm pkg set scripts.dev="npm start" +``` + +Now we're going to configure our API. Let's create our Platformatic configuration +file, **`platformatic.db.json`**: + +```json +{ + "server": { + "logger": { + "level": "{PLT_SERVER_LOGGER_LEVEL}" + }, + "hostname": "{PLT_SERVER_HOSTNAME}", + "port": "{PORT}" + }, + "core": { + "connectionString": "{DATABASE_URL}" + }, + "migrations": { + "dir": "./migrations" + } +} +``` + +Now we'll create a **`.env`** file with settings for our configuration to use: + +``` +PORT=3042 +PLT_SERVER_HOSTNAME=127.0.0.1 +PLT_SERVER_LOGGER_LEVEL=info +DATABASE_URL=sqlite://./movie-quotes.sqlite +``` + +:::info + +Take a look at the [Configuration reference](/reference/configuration.md) +to see all the supported configuration settings. + +::: + +### Define the database schema + +Let's create a new directory to store our migration files: + +```bash +mkdir migrations +``` + +Then we'll create a migration file named **`001.do.sql`** in the **`migrations`** +directory: + +```sql +CREATE TABLE quotes ( + id INTEGER PRIMARY KEY, + quote TEXT NOT NULL, + said_by VARCHAR(255) NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +``` + +Let's also create `.gitignore` so that we avoid accidentally committing our +SQLite database: + +```bash +echo '*.sqlite' > .gitignore +``` + +Now we can start the Platformatic DB server: + +```bash +npm run dev +``` + +Our Platformatic DB server should start, and we'll see messages like these: + +``` +[11:26:48.772] INFO (15235): running 001.do.sql +[11:26:48.864] INFO (15235): server listening + url: "http://127.0.0.1:3042" +``` + +Let's open a new terminal and make a request to our server's REST API that +creates a new quote: + +```bash +curl --request POST --header "Content-Type: application/json" \ + -d "{ \"quote\": \"Toto, I've got a feeling we're not in Kansas anymore.\", \"saidBy\": \"Dorothy Gale\" }" \ + http://localhost:3042/quotes +``` + +We should receive a response like this from the API: + +```json +{"id":1,"quote":"Toto, I've got a feeling we're not in Kansas anymore.","saidBy":"Dorothy Gale","createdAt":"2022-09-13 10:39:35"} +``` + +### Create an entity relationship + +Now let's create a migration file named **`002.do.sql`** in the **`migrations`** +directory: + +```sql +CREATE TABLE movies ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE +); + +-- TODO: Add a foreign key constraint so quotes.movie_id must exist in movies.id +ALTER TABLE quotes ADD COLUMN movie_id INTEGER REFERENCES movies(id); +``` + +This SQL will create a new `movies` database table and also add a `movie_id` +column to the `quotes` table. This will allow us to store movie data in the +`movies` table and then reference them by ID in our `quotes` table. + +Let's stop the Platformatic DB server with `Ctrl + C`, and then start it again: + +```bash +npm run dev +``` + +The new migration should be automatically applied and we'll see the log message +`running 002.do.sql`. + +Our Platformatic DB server also provides a GraphQL API. Let's open up the GraphiQL +application in our web browser: + +> http://localhost:3042/graphiql + +Now let's run this query with GraphiQL to add the movie for the quote that we +added earlier: + +```graphql +mutation { + saveMovie(input: { name: "The Wizard of Oz" }) { + id + } +} +``` + +We should receive a response like this from the API: + +```json +{ + "data": { + "saveMovie": { + "id": "1" + } + } +} +``` + +Now we can update our quote to reference the movie: + +```graphql +mutation { + saveQuote(input: { id: 1, movieId: 1 }) { + id + quote + saidBy + createdAt + movie { + id + name + } + } +} +``` + +We should receive a response like this from the API: + +```json +{ + "data": { + "saveQuote": { + "id": "1", + "quote": "Toto, I've got a feeling we're not in Kansas anymore.", + "saidBy": "Dorothy Gale", + "movie": { + "id": "1", + "name": "The Wizard of Oz" + } + } + } +} +``` + +Our Platformatic DB server has automatically identified the relationship +between our `quotes` and `movies` database tables. This allows us to make +GraphQL queries that retrieve quotes and their associated movies at the same +time. For example, to retrieve all quotes from our database we can run: + +```graphql +query { + quotes { + id + quote + saidBy + createdAt + movie { + id + name + } + } +} +``` + +To view the GraphQL schema that's generated for our API by Platformatic DB, +we can run this command in our terminal: + +```bash +npx platformatic db schema graphql +``` + +The GraphQL schema shows all of the queries and mutations that we can run +against our GraphQL API, as well as the types of data that it expects as input. + +### Populate the database + +Our movie quotes database is looking a little empty! We're going to create a +"seed" script to populate it with some data. + +Let's create a new file named **`seed.js`** and copy and paste in this code: + +```javascript +'use strict' + +const quotes = [ + { + quote: "Toto, I've got a feeling we're not in Kansas anymore.", + saidBy: 'Dorothy Gale', + movie: 'The Wizard of Oz' + }, + { + quote: "You're gonna need a bigger boat.", + saidBy: 'Martin Brody', + movie: 'Jaws' + }, + { + quote: 'May the Force be with you.', + saidBy: 'Han Solo', + movie: 'Star Wars' + }, + { + quote: 'I have always depended on the kindness of strangers.', + saidBy: 'Blanche DuBois', + movie: 'A Streetcar Named Desire' + } +] + +module.exports = async function ({ entities, db, sql }) { + for (const values of quotes) { + const movie = await entities.movie.save({ input: { name: values.movie } }) + + console.log('Created movie:', movie) + + const quote = { + quote: values.quote, + saidBy: values.saidBy, + movieId: movie.id + } + + await entities.quote.save({ input: quote }) + + console.log('Created quote:', quote) + } +} +``` + + + +:::info +Take a look at the [Seed a Database](/guides/seed-a-database.md) guide to learn more +about how database seeding works with Platformatic DB. +::: + +Let's stop our Platformatic DB server running and remove our SQLite database: + +``` +rm movie-quotes.db +``` + +Now let's create a fresh SQLite database by running our migrations: + +```bash +npx platformatic db migrate +``` + +And then let's populate the `quotes` and `movies` tables with data using our +seed script: + +```bash +npx platformatic db seed seed.js +``` + +Our database is full of data, but we don't have anywhere to display it. It's +time to start building our frontend! + +## Build the frontend + +We're now going to use [Astro](https://astro.build/) to build our frontend +application. If you've not used it before, you might find it helpful +to read [this overview](https://docs.astro.build/en/core-concepts/astro-components/) +on how Astro components are structured. + +:::tip +Astro provide some extensions and tools to help improve your +[Editor Setup](https://docs.astro.build/en/editor-setup/) when building an +Astro application. +::: + +### Create an Astro application + +In the root of our project, let's create a new directory for our frontent +application: + +```bash +mkdir -p apps/movie-quotes-frontend/ + +cd apps/movie-quotes-frontend/ +``` + +And then we'll create a new `package.json` file: + +```bash +npm init --yes +``` + +Now we can install [astro](https://www.npmjs.com/package/astro) as a dependency: + +```bash +npm install --save-dev astro +``` + +Then let's set up some npm run scripts for convenience: + +```bash +npm pkg delete scripts.test +npm pkg set scripts.dev="astro dev --port 3000" +npm pkg set scripts.start="astro dev --port 3000" +npm pkg set scripts.build="astro build" +``` + +Now we'll create our Astro configuration file, **`astro.config.mjs`** and +copy and paste in this code: + +```javascript +import { defineConfig } from 'astro/config' + +// https://astro.build/config +export default defineConfig({ + output: 'server' +}) +``` + +And we'll also create a **`tsconfig.json`** file and add in this configuration: + +```json +{ + "extends": "astro/tsconfigs/base", + "compilerOptions": { + "types": ["astro/client"] + } +} +``` + +> We won't be writing our frontend application with TypeScript, but adding this +> configuration file allows Astro to provide TODO +> https://docs.astro.build/en/guides/typescript/ + +Now let's create the directories where we'll be adding the components for our +frontend application: + +```bash +mkdir -p src/pages src/layouts src/components +``` + +And inside the **`src/pages`** directory let's create our first page, **`index.astro`**: + +```astro +

Movie Quotes

+``` + +Now we can start up the Astro development server with: + +```bash +npm run dev +``` + +And then load up the frontend in our browser at [http://localhost:3000](http://localhost:3000) + +### Create a layout + +In the **`src/layouts`** directory, let's create a new file named **`Layout.astro`**: + +```astro +--- +export interface Props { + title: string; + page?: string; +} +const { title, page } = Astro.props; +--- + + + + + + + {title} + + +
+

🎬 Movie Quotes

+
+ +
+ +
+ + +``` + +The code between the `---` is known as the component script, and the +code after that is the component template. The component script will *only* run +on the server side when a web browser makes a request. The component template +is rendered server side and sent back as an HTML response to the web browser. + +Now we'll update **`src/pages/index.astro`** to use this `Layout` component. +Let's replace the contents of **`src/pages/index.astro`** with this code: + +```astro +--- +import Layout from '../layouts/Layout.astro'; +--- + + +
+

We'll list all the movie quotes here.

+
+
+``` + +### Integrate the urql GraphQL client + +We're now going to integrate the [URQL](https://formidable.com/open-source/urql/) +GraphQL client into our frontend application. This will allow us to run queries +and mutations against our Platformatic GraphQL API. + +Let's first install [@urql/core](https://www.npmjs.com/package/@urql/core) and +[graphql](https://www.npmjs.com/package/graphql) as project dependencies: + +```bash +npm install @urql/core graphql +``` + +Then let's create a new **`.env`** file and add this configuration: + +``` +PUBLIC_GRAPHQL_API_ENDPOINT=http://127.0.0.1:3042/graphql +``` + +Now we'll create a new directory: + +```bash +mkdir src/lib +``` + +And then create a new file named **`src/lib/quotes-api.js`**. In that file we'll +create a new URQL client: + +```javascript +// src/lib/quotes-api.js + +import { createClient } from '@urql/core'; + +const graphqlClient = createClient({ + url: import.meta.env.PUBLIC_GRAPHQL_API_ENDPOINT, + requestPolicy: "network-only" +}); +``` + +We'll also add a thin wrapper around the client that does some basic error +handling for us: + +```javascript +// src/lib/quotes-api.js + +async function graphqlClientWrapper(method, gqlQuery, queryVariables = {}) { + const queryResult = await graphqlClient[method]( + gqlQuery, + queryVariables + ).toPromise(); + + if (queryResult.error) { + console.error("GraphQL error:", queryResult.error); + } + + return { + data: queryResult.data, + error: queryResult.error, + }; +} + +export const quotesApi = { + async query(gqlQuery, queryVariables = {}) { + return await graphqlClientWrapper("query", gqlQuery, queryVariables); + }, + async mutation(gqlQuery, queryVariables = {}) { + return await graphqlClientWrapper("mutation", gqlQuery, queryVariables); + } +} +``` + +And lastly, we'll export `gql` from the `@urql/core` package, to make it +simpler for us to write GraphQL queries in our pages: + +```javascript +// src/lib/quotes-api.js + +export { gql } from "@urql/core"; +``` + +Stop the Astro dev server and then start it again so it picks up the **`.env`** +file: + +```bash +npm run dev +``` + +### Display all quotes + +Let's display all the movie quotes in **`src/pages/index.astro`**. + +First, we'll update the component script at the top and add in a query to +our GraphQL API for quotes: + +```astro +--- +import Layout from '../layouts/Layout.astro'; +// highlight-start +import { quotesApi, gql } from '../lib/quotes-api'; + +const { data } = await quotesApi.query(gql` + query { + quotes { + id + quote + saidBy + createdAt + movie { + id + name + } + } + } +`); + +const quotes = data?.quotes || []; +// highlight-end +--- +``` + +Then we'll update the component template to display the quotes: + +```astro + +
+// highlight-start + {quotes.length > 0 ? quotes.map((quote) => ( +
+
+

{quote.quote}

+
+

+ — {quote.saidBy}, {quote.movie?.name} +

+
+ Added {new Date(quote.createdAt).toUTCString()} +
+
+ )) : ( +

No movie quotes have been added.

+ )} +// highlight-end +
+
+``` + +And just like that, we have all the movie quotes displaying on the page! + +### Integrate Tailwind for styling + +Automatically add the [@astrojs/tailwind integration](https://docs.astro.build/en/guides/integrations-guide/tailwind/): + +```bash +npx astro add tailwind --yes +``` + +Add the Tailwind CSS [Typography](https://tailwindcss.com/docs/typography-plugin) +and [Forms](https://github.com/tailwindlabs/tailwindcss-forms) plugins: + +```bash +npm install --save-dev @tailwindcss/typography @tailwindcss/forms +``` + +Import the plugins in our Tailwind configuration file: + +```javascript +// tailwind.config.cjs + +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'], + theme: { + extend: {} + }, +// highlight-start + plugins: [ + require('@tailwindcss/forms'), + require('@tailwindcss/typography') + ] +// highlight-end +} +``` + +Stop the Astro dev server and then start it again so it picks up all the +configuration changes: + +```bash +npm run dev +``` + +### Style the listing page + +To style our listing page, let's add CSS classes to the component template in +**`src/layouts/Layout.astro`**: + +```astro +--- +export interface Props { + title: string; + page?: string; +} + +const { title, page } = Astro.props; + +// highlight-next-line +const navActiveClasses = "font-bold bg-yellow-400 no-underline"; +--- + + + + + + + {title} + +// highlight-next-line + +// highlight-next-line +
+

🎬 Movie Quotes

+
+// highlight-next-line + +// highlight-next-line +
+ +
+ + +``` + +Then let's add CSS classes to the component template in **`src/pages/index.astro`**: + +```astro + +
+ {quotes.length > 0 ? quotes.map((quote) => ( +// highlight-next-line +
+// highlight-next-line +
+// highlight-next-line +

{quote.quote}

+
+// highlight-next-line +

+ — {quote.saidBy}, {quote.movie?.name} +

+// highlight-next-line +
+// highlight-next-line + Added {new Date(quote.createdAt).toUTCString()} +
+
+ )) : ( +

No movie quotes have been added.

+ )} +
+
+``` + +Our listing page is now looking much more user friendly! + +### Create an add quote page + +We're going to create a form component that we can use for adding and editing +quotes. + +First let's create a new component file, **`src/components/QuoteForm.astro`**: + +```astro +--- +export interface QuoteFormData { + id?: number; + quote?: string; + saidBy?: string; + movie?: string; +} + +export interface Props { + action: string; + values?: QuoteFormData; + saveError?: boolean; + loadError?: boolean; + submitLabel: string; +} + +const { action, values = {}, saveError, loadError, submitLabel } = Astro.props; +--- + +{saveError &&

There was an error saving the quote. Please try again.

} +{loadError &&

There was an error loading the quote. Please try again.

} + +
+ + + + +
+``` + +Create a new page file, **`src/pages/add.astro`**: + +```astro +--- +import Layout from '../layouts/Layout.astro'; +import QuoteForm from '../components/QuoteForm.astro'; +import type { QuoteFormData } from '../components/QuoteForm.astro'; + +let formData: QuoteFormData = {}; +let saveError = false; +--- + + +
+

Add a quote

+ +
+
+``` + +And now let's add a link to this page in the layout navigation in **`src/layouts/Layout.astro`**: + +```astro + +``` + +### Send form data to the API + +When a user submits the add quote form we want to send the form data to our API +so it can then save it to our database. Let's wire that up now. + +First we're going to create a new file, **`src/lib/request-utils.js`**: + +```javascript +export function isPostRequest (request) { + return request.method === 'POST' +} + +export async function getFormData (request) { + const formData = await request.formData() + + return Object.fromEntries(formData.entries()) +} +``` + + + +Then let's update the component script in **`src/pages/add.astro`** to use +these new request utility functions: + +```astro +--- +import Layout from '../layouts/Layout.astro'; +import QuoteForm from '../components/QuoteForm.astro'; +import type { QuoteFormData } from '../components/QuoteForm.astro'; + +// highlight-next-line +import { isPostRequest, getFormData } from '../lib/request-utils'; + +let formData: QuoteFormData = {}; +let saveError = false; + +// highlight-start +if (isPostRequest(Astro.request)) { + formData = await getFormData(Astro.request); +} +// highlight-end +--- +``` + + + +When we create a new quote entity record via our API, we need to include a +`movieId` field that references a movie entity record. This means that when a +user submits the add quote form we need to: + +- Check if a movie entity record already exists with that movie name +- Return the movie `id` if it does exist +- If it doesn't exist, create a new movie entity record and return the movie ID + +Let's update the `import` statement at the top of **`src/lib/quotes-api.js`** + +```diff +-import { createClient } from '@urql/core' ++import { createClient, gql } from '@urql/core' +``` + +And then add a new method that will return a movie ID for us: + +```javascript +async function getMovieId (movieName) { + movieName = movieName.trim() + + let movieId = null + + // Check if a movie already exists with the provided name. + const queryMoviesResult = await quotesApi.query( + gql` + query ($movieName: String!) { + movies(where: { name: { eq: $movieName } }) { + id + } + } + `, + { movieName } + ) + + if (queryMoviesResult.error) { + return null + } + + const movieExists = queryMoviesResult.data?.movies.length === 1 + if (movieExists) { + movieId = queryMoviesResult.data.movies[0].id + } else { + // Create a new movie entity record. + const saveMovieResult = await quotesApi.mutation( + gql` + mutation ($movieName: String!) { + saveMovie(input: { name: $movieName }) { + id + } + } + `, + { movieName } + ) + + if (saveMovieResult.error) { + return null + } + + movieId = saveMovieResult.data?.saveMovie.id + } + + return movieId +} +``` + +And let's export it too: + +```javascript +export const quotesApi = { + async query (gqlQuery, queryVariables = {}) { + return await graphqlClientWrapper('query', gqlQuery, queryVariables) + }, + async mutation (gqlQuery, queryVariables = {}) { + return await graphqlClientWrapper('mutation', gqlQuery, queryVariables) + }, +// highlight-next-line + getMovieId +} +``` + +Now we can wire up the last parts in the **`src/pages/add.astro`** component +script: + +```astro +--- +import Layout from '../layouts/Layout.astro'; +import QuoteForm from '../components/QuoteForm.astro'; +import type { QuoteFormData } from '../components/QuoteForm.astro'; + +// highlight-next-line +import { quotesApi, gql } from '../lib/quotes-api'; +import { isPostRequest, getFormData } from '../lib/request-utils'; + +let formData: QuoteFormData = {}; +let saveError = false; + +if (isPostRequest(Astro.request)) { + formData = await getFormData(Astro.request); + +// highlight-start + const movieId = await quotesApi.getMovieId(formData.movie); + + if (movieId) { + const quote = { + quote: formData.quote, + saidBy: formData.saidBy, + movieId, + }; + + const { error } = await quotesApi.mutation(gql` + mutation($quote: QuoteInput!) { + saveQuote(input: $quote) { + id + } + } + `, { quote }); + + if (!error) { + return Astro.redirect('/'); + } else { + saveError = true; + } + } else { + saveError = true; + } +// highlight-end +} +``` + + + +### Add autosuggest for movies + +We can create a better experience for our users by autosuggesting the movie name +when they're adding a new quote. + +Let's open up **`src/components/QuoteForm.astro`** and import our API helper methods +in the component script: + +```astro +import { quotesApi, gql } from '../lib/quotes-api.js'; +``` + +Then let's add in a query to our GraphQL API for all movies: + +```astro +const { data } = await quotesApi.query(gql` + query { + movies { + name + } + } +`); + +const movies = data?.movies || []; +``` + +Now lets update the *Movie* field in the component template to use the +array of movies that we've retrieved from the API: + +```astro + +``` + + + +### Create an edit quote page + +Let's create a new directory, **`src/pages/edit/`**: + +```bash +mkdir src/pages/edit/ +``` + +And inside of it, let's create a new page, **`[id].astro`**: + +```astro +--- +import Layout from '../../layouts/Layout.astro'; +import QuoteForm, { QuoteFormData } from '../../components/QuoteForm.astro'; + +const id = Number(Astro.params.id); + +let formValues: QuoteFormData = {}; +let loadError = false; +let saveError = false; +--- + + +
+

Edit quote

+ +
+
+``` + +You'll see that we're using the same `QuoteForm` component that our add quote +page uses. Now we're going to wire up our edit page so that it can load an +existing quote from our API and save changes back to the API when the form is +submitted. + +In the **`[id.astro]`** component script, let's add some code to take care of +these tasks: + +```astro +--- +import Layout from '../../layouts/Layout.astro'; +import QuoteForm, { QuoteFormData } from '../../components/QuoteForm.astro'; + +// highlight-start +import { quotesApi, gql } from '../../lib/quotes-api'; +import { isPostRequest, getFormData } from '../../lib/request-utils'; +// highlight-end + +const id = Number(Astro.params.id); + +let formValues: QuoteFormData = {}; +let loadError = false; +let saveError = false; + +// highlight-start +if (isPostRequest(Astro.request)) { + const formData = await getFormData(Astro.request); + formValues = formData; + + const movieId = await quotesApi.getMovieId(formData.movie); + + if (movieId) { + const quote = { + id, + quote: formData.quote, + saidBy: formData.saidBy, + movieId, + }; + + const { error } = await quotesApi.mutation(gql` + mutation($quote: QuoteInput!) { + saveQuote(input: $quote) { + id + } + } + `, { quote }); + + if (!error) { + return Astro.redirect('/'); + } else { + saveError = true; + } + } else { + saveError = true; + } +} else { + const { data } = await quotesApi.query(gql` + query($id: ID!) { + getQuoteById(id: $id) { + id + quote + saidBy + movie { + id + name + } + } + } + `, { id }); + + if (data?.getQuoteById) { + formValues = { + ...data.getQuoteById, + movie: data.getQuoteById.movie.name + }; + } else { + loadError = true; + } +} +// highlight-end +--- +``` + + + +Load up [http://localhost:3000/edit/1](http://localhost:3000/edit/1) in your +browser to test out the edit quote page. + +Now we're going to add edit links to the quotes listing page. Let's start by +creating a new component **`src/components/QuoteActionEdit.astro`**: + +```astro +--- +export interface Props { + id: number; +} + +const { id } = Astro.props; +--- + + + + + + Edit + +``` + +Then let's import this component and use it in our listing page, +**`src/pages/index.astro`**: + +```astro +--- +import Layout from '../layouts/Layout.astro'; +// highlight-next-line +import QuoteActionEdit from '../components/QuoteActionEdit.astro'; +import { quotesApi, gql } from '../lib/quotes-api'; + +// ... +--- + + +
+ {quotes.length > 0 ? quotes.map((quote) => ( +
+ ... +
+// highlight-start + + + + Added {new Date(quote.createdAt).toUTCString()} +// highlight-end +
+
+ )) : ( +

No movie quotes have been added.

+ )} +
+
+``` + +### Add delete quote functionality + +Our Movie Quotes app can create, retrieve and update quotes. Now we're going +to implement the D in CRUD — delete! + +First let's create a new component, **`src/components/QuoteActionDelete.astro`**: + +```astro +--- +export interface Props { + id: number; +} + +const { id } = Astro.props; +--- +
+ +
+``` + + + +And then we'll drop it into our listing page, **`src/pages/index.astro`**: + +```astro +--- +import Layout from '../layouts/Layout.astro'; +import QuoteActionEdit from '../components/QuoteActionEdit.astro'; +// highlight-next-line +import QuoteActionDelete from '../components/QuoteActionDelete.astro'; +import { quotesApi, gql } from '../lib/quotes-api'; + +// ... +--- + + +
+ {quotes.length > 0 ? quotes.map((quote) => ( +
+ ... +
+ + +// highlight-next-line + + + Added {new Date(quote.createdAt).toUTCString()} +
+
+... +``` + +At the moment when a delete form is submitted from our listing page, we get +an Astro 404 page. Let's fix this by creating a new directory, **`src/pages/delete/`**: + +```bash +mkdir src/pages/delete/ +``` + +And inside of it, let's create a new page, **`[id].astro`**: + +```astro +--- +import Layout from '../../layouts/Layout.astro'; + +import { quotesApi, gql } from '../../lib/quotes-api'; +import { isPostRequest } from '../../lib/request-utils'; + +if (isPostRequest(Astro.request)) { + const id = Number(Astro.params.id); + + const { error } = await quotesApi.mutation(gql` + mutation($id: ID!) { + deleteQuotes(where: { id: { eq: $id }}) { + id + } + } + `, { id }); + + if (!error) { + return Astro.redirect('/'); + } +} +--- + +
+

Delete quote

+

There was an error deleting the quote. Please try again.

+
+
+``` + + + +Now if we click on a delete quote button on our listings page, it should call our +GraphQL API to delete the quote. To make this a little more user friendly, let's +add in a confirmation dialog so that users don't delete a quote by accident. + + + + +Let's create a new directory, **`src/scripts/`**: + +```bash +mkdir src/scripts/ +``` + +And inside of that directory let's create a new file, **`quote-actions.js`**: + +```javascript +// src/scripts/quote-actions.js + +export function confirmDeleteQuote (form) { + if (confirm('Are you sure want to delete this quote?')) { + form.submit() + } +} +``` + +Then we can pull it in as client side JavaScript on our listing page, +**`src/pages/index.astro`**: + +```astro + + ... + + + +``` + + + +## Build a "like" quote feature + +We've built all the basic CRUD (Create, Retrieve, Update & Delete) features +into our application. Now let's build a feature so that users can interact +and "like" their favourite movie quotes. + +To build this feature we're going to add custom functionality to our API +and then add a new component, along with some client side JavaScript, to +our frontend. + +### Create an API migration + +We're now going to work on the code for API, under the **`apps/movie-quotes-api`** +directory. + +First let's create a migration that adds a `likes` column to our `quotes` +database table. We'll create a new migration file, **`migrations/003.do.sql`**: + +```sql +ALTER TABLE quotes ADD COLUMN likes INTEGER default 0; +``` + +This migration will automatically be applied when we next start our Platformatic +API. + +### Create an API plugin + +To add custom functionality to our Platformatic API, we need to create a +[Fastify plugin](https://www.fastify.io/docs/latest/Reference/Plugins/) and +update our API configuration to use it. + +Let's create a new file, **`plugin.js`**, and inside it we'll add the skeleton +structure for our plugin: + +```javascript +// plugin.js + +'use strict' + +module.exports = async function plugin (app) { + app.log.info('plugin loaded') +} +``` + +Now let's register our plugin in our API configuration file, **`platformatic.db.json`**: + +```json +{ + ... + "migrations": { + "dir": "./migrations" +// highlight-start + }, + "plugin": { + "path": "./plugin.js" + } +// highlight-end +} +``` + +And then we'll start up our Platformatic API: + +```bash +npm run dev +``` + +We should see log messages that tell us that our new migration has been +applied and our plugin has been loaded: + +``` +[10:09:20.052] INFO (146270): running 003.do.sql +[10:09:20.129] INFO (146270): plugin loaded +[10:09:20.209] INFO (146270): server listening + url: "http://127.0.0.1:3042" +``` + +Now it's time to start adding some custom functionality inside our plugin. + +### Add a REST API route + + + +We're going to add a REST route to our API that increments the count of +likes for a specific quote: `/quotes/:id/like` + +First let's add [fluent-json-schema](https://www.npmjs.com/package/fluent-json-schema) as a dependency for our API: + +```bash +npm install fluent-json-schema +``` + +We'll use `fluent-json-schema` to help us generate a JSON Schema. We can then +use this schema to validate the request path parameters for our route (`id`). + +Now let's add our REST API route in **`plugin.js`**: + +```javascript +'use strict' + +// highlight-next-line +const S = require('fluent-json-schema') + +module.exports = async function plugin (app) { + app.log.info('plugin loaded') + + // This JSON Schema will validate the request path parameters. + // It reuses part of the schema that Platormatic DB has + // automatically generated for our Quote entity. +// highlight-start + const schema = { + params: S.object().prop('id', app.getSchema('Quote').properties.id) + } + + app.post('/quotes/:id/like', { schema }, async function (request, response) { + return {} + }) +// highlight-end +} +``` + +We can now make a `POST` request to our new API route: + +```bash +curl --request POST http://localhost:3042/quotes/1/like +``` + +:::info +Learn more about how validation works in the +[Fastify validation documentation](https://www.fastify.io/docs/latest/Reference/Validation-and-Serialization/). +::: + +Our API route is currently returning an empty object (`{}`). Let's wire things +up so that it increments the number of likes for the quote with the specified ID. +To do this we'll add a new function inside of our plugin: + +```javascript +module.exports = async function plugin (app) { + app.log.info('plugin loaded') + +// highlight-start + async function incrementQuoteLikes (id) { + const { db, sql } = app.platformatic + + const result = await db.query(sql` + UPDATE quotes SET likes = likes + 1 WHERE id=${id} RETURNING likes + `) + + return result[0]?.likes + } +// highlight-end + + // ... +} +``` + +And then we'll call that function in our route handler function: + +```javascript +app.post('/quotes/:id/like', { schema }, async function (request, response) { +// highlight-next-line + return { likes: await incrementQuoteLikes(request.params.id) } +}) +``` + +Now when we make a `POST` request to our API route: + +```bash +curl --request POST http://localhost:3042/quotes/1/like +``` + +We should see that the `likes` value for the quote is incremented every time +we make a request to the route. + +```json +{"likes":1} +``` + + + +### Add a GraphQL API mutation + +We can add a `likeQuote` mutation to our GraphQL API by reusing the +`incrementQuoteLikes` function that we just created. + +Let's add this code at the end of our plugin, inside **`plugin.js`**: + +```javascript +module.exports = async function plugin (app) { + // ... + +// highlight-start + app.graphql.extendSchema(` + extend type Mutation { + likeQuote(id: ID!): Int + } + `) + + app.graphql.defineResolvers({ + Mutation: { + likeQuote: async (_, { id }) => await incrementQuoteLikes(id) + } + }) +// highlight-end +} +``` + +The code we've just added extends our API's GraphQL schema and defines +a corresponding resolver for the `likeQuote` mutation. + +We can now load up GraphiQL in our web browser and try out our new `likeQuote` +mutation with this GraphQL query: + +```graphql +mutation { + likeQuote(id: 1) +} +``` + +:::info +Learn more about how to extend the GraphQL schema and define resolvers in the +[Mercurius API documentation](https://mercurius.dev/#/docs/api/options). +::: + +### Enable CORS on the API + +When we build "like" functionality into our frontend, we'll be making a client +side HTTP request to our GraphQL API. Our backend API and our frontend are running +on different origins, so we need to configure our API to allow requests from +the frontend. This is known as Cross-Origin Resource Sharing (CORS). + +To enable CORS on our API, let's open up our API's **`.env`** file and add in +a new setting: + +``` +PLT_SERVER_CORS_ORIGIN=http://localhost:3000 +``` + +The value of `PLT_SERVER_CORS_ORIGIN` is our frontend application's origin. + +Now we can add a `cors` configuration object in our API's configuration file, +**`platformatic.db.json`**: + +```json +{ + "server": { + "logger": { + "level": "{PLT_SERVER_LOGGER_LEVEL}" + }, + "hostname": "{PLT_SERVER_HOSTNAME}", + "port": "{PORT}", +// highlight-start + "cors": { + "origin": "{PLT_SERVER_CORS_ORIGIN}" + } +// highlight-end + }, + ... +} +``` + +The HTTP responses from all endpoints on our API will now include the header: + +``` +access-control-allow-origin: http://localhost:3000 +``` + +This will allow JavaScript running on web pages under the `http://localhost:3000` +origin to make requests to our API. + +### Add like quote functionality + +Now that our API supports "liking" a quote, let's integrate it as a feature in +our frontend. + +First we'll create a new component, **`src/components/QuoteActionLike.astro`**: + +```astro +--- +export interface Props { + id: number; + likes: number; +} + +const { id, likes } = Astro.props; +--- + + + +``` + +And in our listing page, **`src/pages/index.astro`**, let's import our new +component and add it into the interface: + +```astro +--- +import Layout from '../layouts/Layout.astro'; +import QuoteActionEdit from '../components/QuoteActionEdit.astro'; +import QuoteActionDelete from '../components/QuoteActionDelete.astro'; +// highlight-next-line +import QuoteActionLike from '../components/QuoteActionLike.astro'; +import { quotesApi, gql } from '../lib/quotes-api'; + +// ... +--- + + +
+ {quotes.length > 0 ? quotes.map((quote) => ( +
+ ... +
+ +// highlight-next-line + + + + + Added {new Date(quote.createdAt).toUTCString()} +
+
+... +``` + +Then let's update the GraphQL query in this component's script to retrieve the +`likes` field for all quotes: + +```javascript +const { data } = await quotesApi.query(gql` + query { + quotes { + id + quote + saidBy +// highlight-next-line + likes + createdAt + movie { + id + name + } + } + } +`); +``` + +Now we have the likes showing for each quote, let's wire things up so that +clicking on the like component for a quote will call our API and add a like. + +Let's open up **`src/scripts/quote-actions.js`** and add a new function that +makes a request to our GraphQL API: + +```javascript +// highlight-next-line +import { quotesApi, gql } from '../lib/quotes-api.js' + +export function confirmDeleteQuote (form) { + if (confirm('Are you sure want to delete this quote?')) { + form.submit() + } +} + +// highlight-start +export async function likeQuote (likeQuote) { + likeQuote.classList.add('liked') + likeQuote.classList.remove('cursor-pointer') + + const id = Number(likeQuote.dataset.quoteId) + + const { data } = await quotesApi.mutation(gql` + mutation($id: ID!) { + likeQuote(id: $id) + } + `, { id }) + + if (data?.likeQuote) { + likeQuote.querySelector('.likes-count').innerText = data.likeQuote + } +} +// highlight-end +``` + +And then let's attach the `likeQuote` function to the click event for each +like quote component on our listing page. We can do this by adding a little +extra code inside the ` +``` + +### Sort the listing by top quotes + +Now that users can like their favourite quotes, as a final step, we'll allow +for sorting quotes on the listing page by the number of likes they have. + +Let's update **`src/pages/index.astro`** to read a `sort` query string parameter +and use it the GraphQL query that we make to our API: + +```astro +--- +// ... + +// highlight-start +const allowedSortFields = ["createdAt", "likes"]; +const searchParamSort = new URL(Astro.request.url).searchParams.get("sort"); +const sort = allowedSortFields.includes(searchParamSort) ? searchParamSort : "createdAt"; +// highlight-end + +const { data } = await quotesApi.query(gql` + query { +// highlight-next-line + quotes(orderBy: {field: ${sort}, direction: DESC}) { + id + quote + saidBy + likes + createdAt + movie { + id + name + } + } + } +`); + +const quotes = data?.quotes || []; +--- +// highlight-next-line + +... +``` + +Then let's replace the 'All quotes' link in the `